package com.ruoyi.pay.service.impl;

import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.IdUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.Sign;
import cn.hutool.crypto.asymmetric.SignAlgorithm;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.config.WxPayV3Config;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.constant.OperationType;
import com.ruoyi.common.constant.WxPayV3Url;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.BaseOrder;
import com.ruoyi.common.core.domain.entity.system.*;
import com.ruoyi.common.core.domain.entity.wsbean.PintuanStatusMsg;
import com.ruoyi.common.core.domain.model.WxRefundReqBody;
import com.ruoyi.common.enums.BoatStatus;
import com.ruoyi.common.enums.OrderStatus;
import com.ruoyi.common.enums.PayStatus;
import com.ruoyi.common.enums.PintuanStatus;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.CacheUtils;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.common.utils.uuid.ShortIdUtils;
import com.ruoyi.handler.websocket.WebSocketMessageHandler;
import com.ruoyi.manager.WebSocketChannelManager;
import com.ruoyi.pay.service.IWxPayV3Service;
import com.ruoyi.system.mapper.*;
import com.ruoyi.system.service.*;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import io.netty.channel.Channel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;

@Service
@Transactional
@Slf4j
public class WxPayV3ServiceImpl implements IWxPayV3Service {

    private static final DecimalFormat df = new DecimalFormat("#");

    ConcurrentMap<String, Timer> payStatusQueryTimerMap = new ConcurrentHashMap<>();

    @Autowired
    private WxPayV3Config wxPayV3Config;
    //微信专业httpClient
    private static CloseableHttpClient httpClient;
    //生成签名用
    private static Sign sign;
    //证书管理器
    private static CertificatesManager certificatesManager;

    @Resource
    private SysOrderMapper sysOrderMapper;
    @Resource
    private SysUserMapper sysUserMapper;

    @Value("${miniprogram.onBoatCountdownDuration}")
    private long onBoatCountdownDuration;

    @Value("${miniprogram.pintuanCountdownDuration}")
    private long pintuanCountdownDuration;

    @Resource
    private SysJishiOrderMapper sysJishiOrderMapper;

    @Resource
    private SysTaocanOrderMapper sysTaocanOrderMapper;

    @Autowired
    private IUserBoatOrderInfoService userBoatOrderInfoService;

    @Resource
    private SysPintuanChildrenOrderMapper sysPintuanChildrenOrderMapper;

    @Resource
    private SysPintuanParentOrderMapper sysPintuanParentOrderMapper;

    @Resource
    private SysBoatMapper sysBoatMapper;

    @Autowired
    private ISysBoatService sysBoatService;

    @Autowired
    private IScenicDailyIncomeService scenicDailyIncomeService;

    @Autowired
    private ISysUserService sysUserService;

    private SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm");

    //初始化微信支付的参数
    @PostConstruct
    public void init() {
        try {
            //获取商户私钥
            PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(getFileInputStream(wxPayV3Config.getCertKeyPath()));
            // 获取证书管理器实例
            certificatesManager = CertificatesManager.getInstance();
            sign = SecureUtil.sign(SignAlgorithm.SHA256withRSA, merchantPrivateKey.getEncoded(), null);
            // 向证书管理器增加需要自动更新平台证书的商户信息
            certificatesManager.putMerchant(wxPayV3Config.getMchId(), new WechatPay2Credentials(wxPayV3Config.getMchId(),
                    new PrivateKeySigner(wxPayV3Config.getSerialnumber(), merchantPrivateKey)), wxPayV3Config.getApiKey3().getBytes(StandardCharsets.UTF_8));
            // 从证书管理器中获取verifier
            Verifier verifier = certificatesManager.getVerifier(wxPayV3Config.getMchId());
            //用于构造HttpClient
            WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                    .withMerchant(wxPayV3Config.getMchId(), wxPayV3Config.getSerialnumber(), merchantPrivateKey)
                    .withValidator(new WechatPay2Validator(verifier));
            httpClient = builder.build();
            log.info("init httpClient: {}", httpClient);
        } catch (Exception e) {
//            throw new RuntimeException(e);
            log.error("初始化微信支付http客户端失败，e: {}", e.toString());
        }
    }

    /**
     * 使用getResourceAsStream直接从resources根路径下获取文件流
     *
     * @param path
     */
    private InputStream getFileInputStream(String path) {
        InputStream in = this.getClass().getResourceAsStream(path);
        return in;
    }


    /**
     * 微信支付统一预下单接口，请查看微信支付官方接口文档: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml
     *
     * @param orderType 订单类型。0：计时收费订单，1：套餐收费订单，2：拼团订单。
     * @param payType   支付类型。0：仅支付押金，1：仅支付订单费用，2：支付押金+费用
     * @return
     */
    @Override
    @Transactional
    public Map<String, Object> prePay(String orderId, int orderType, int payType) {
        log.info("微信小程序JSAPI预支付开始------");

        //准备微信支付接口需要的参数
        //当前订单需支付的金额
        double totalPrice = 0;
        //商品描述
        String description = "";
        //商户订单号
        String outOrderNo = "";

        //当前登录的用户
        SysUser sysUser = null;

        //先判断订单类型。0：计时收费订单，1：套餐收费订单，2：拼团订单。
        if (orderType == 0) { //计时收费订单
            SysJishiOrder sysJishiOrder = sysJishiOrderMapper.selectSysJishiOrderByOrderId(orderId);
            if (Objects.isNull(sysJishiOrder)) {
                throw new ServiceException("创建预支付订单失败,找不到对应的订单信息.");
            }

            sysUser = sysUserMapper.selectUserById(sysJishiOrder.getUserId());
            if (Objects.isNull(sysUser)) {
                throw new ServiceException("创建预支付订单失败,找不到当前订单对应的用户信息.");
            }

            //计时计费订单的支付类型只有：0(仅支付押金),1(仅支付订单费用)两种。
            if (payType == 0) { //仅支付押金
                totalPrice = sysJishiOrder.getOrderDeposit();
                outOrderNo = sysJishiOrder.getDepositOutOrderNo();
                description = "支付计时计费订单押金";
            } else if (payType == 1) { //仅支付订单费用
                totalPrice = sysJishiOrder.getOrderFee();
                //用户在支付计时计费订单的费用时没有重新创建订单，其还是使用在支付押金时创建的订单，所以此时需要另外创建支付订单费用的商户订单编号
                String feeOutOrderNo = IdUtil.simpleUUID();
                sysJishiOrder.setFeeOutOrderNo(feeOutOrderNo);
                sysJishiOrderMapper.updateSysJishiOrder(sysJishiOrder);

                outOrderNo = feeOutOrderNo;
                description = "支付计时计费订单费用";
            } else {
                throw new ServiceException("支付类型参数payType不符合要求,计时计费订单的支付类型只有：0(仅支付押金),1(仅支付订单费用)两种");
            }
        } else if (orderType == 1) { //套餐收费订单
            SysTaocanOrder sysTaocanOrder = sysTaocanOrderMapper.selectSysTaocanOrderByOrderId(orderId);
            if (Objects.isNull(sysTaocanOrder)) {
                throw new ServiceException("创建预支付订单失败,找不到对应的订单信息.");
            }

            sysUser = sysUserMapper.selectUserById(sysTaocanOrder.getUserId());
            if (Objects.isNull(sysUser)) {
                throw new ServiceException("创建预支付订单失败,找不到当前订单对应的用户信息.");
            }

            //套餐计费订单的支付类型只有：2(支付押金+费用),3(支付超时费用)两种。
            if (payType == 2) { //支付押金+费用
                totalPrice = sysTaocanOrder.getOrderTotalPrice();
                outOrderNo = sysTaocanOrder.getDepositAndFeeOutOrderNo();
                description = "支付套餐计费订单的押金及套餐费";
            } else if (payType == 3) { //仅支付超时费用
                totalPrice = sysTaocanOrder.getOvertimeFee();
                //用户在支付计时计费订单的费用时没有重新创建订单，其还是使用在支付押金时创建的订单，所以此时需要另外创建支付订单费用的商户订单编号
                String overtimeFeeOutOrderNo = IdUtil.simpleUUID();
                sysTaocanOrder.setOvertimeFeeOutOrderNo(overtimeFeeOutOrderNo);
                sysTaocanOrderMapper.updateSysTaocanOrder(sysTaocanOrder);

                outOrderNo = overtimeFeeOutOrderNo;
                description = "支付套餐计费订单的超时费用";
            } else {
                throw new ServiceException("支付类型参数payType不符合要求，套餐计费订单的支付类型只有：2(支付押金+费用),3(支付超时费用)两种");
            }

        } else if (orderType == 2) { //拼团订单
            SysPintuanChildrenOrder sysPintuanChildrenOrder = sysPintuanChildrenOrderMapper.selectSysPintuanChildrenOrderByOrderId(orderId);
            if (Objects.isNull(sysPintuanChildrenOrder)) {
                throw new ServiceException("创建预支付订单失败,找不到对应的订单信息.");
            }

            sysUser = sysUserMapper.selectUserById(sysPintuanChildrenOrder.getUserId());
            if (Objects.isNull(sysUser)) {
                throw new ServiceException("创建预支付订单失败,找不到当前订单对应的用户信息.");
            }

            //套餐计费订单的支付类型只有：2(支付押金+费用),3(支付超时费用)两种。
            if (payType == 2) { //支付押金+费用
                totalPrice = sysPintuanChildrenOrder.getOrderTotalPrice();
                outOrderNo = sysPintuanChildrenOrder.getDepositAndFeeOutOrderNo();
                description = "支付拼团订单的押金及套餐费";
            } else if (payType == 3) { //仅支付超时费用
                totalPrice = sysPintuanChildrenOrder.getOvertimeFee();
                //用户在支付计时计费订单的费用时没有重新创建订单，其还是使用在支付押金时创建的订单，所以此时需要另外创建支付订单超时费用的商户订单编号
                String overtimeFeeOutOrderNo = IdUtil.simpleUUID();
                sysPintuanChildrenOrder.setOvertimeFeeOutOrderNo(overtimeFeeOutOrderNo);
                sysPintuanChildrenOrderMapper.updateSysPintuanChildrenOrder(sysPintuanChildrenOrder);

                outOrderNo = overtimeFeeOutOrderNo;
                description = "支付拼团订单的超时费用";
            } else {
                throw new ServiceException("支付类型参数payType不符合要求，套餐计费订单的支付类型只有：2(支付押金+费用),3(支付超时费用)两种");
            }
        } else {
            throw new ServiceException("订单类型参数orderType不符合要求.");
        }

        if (Objects.isNull(sysUser)) {
            throw new ServiceException("创建预支付订单失败,找不到当前订单对应的用户信息.");
        }

        Map<String, Object> resultMap = new HashMap<>();

        JSONObject obj = new JSONObject();
        //应用ID
        obj.put("appid", wxPayV3Config.getAppId());
        //直连商户号
        obj.put("mchid", wxPayV3Config.getMchId());
        //商品描述
        obj.put("description", description);
        //商户订单号
        obj.put("out_trade_no", outOrderNo);
        String attachStr = JSONObject.toJSONString(new WxPayAttachObject(orderId, orderType, payType));
        //设置附加数据。注意：附加数据是字符串格式，且字符串长度不能超过128
        obj.put("attach", attachStr);
        //通知地址
        obj.put("notify_url", wxPayV3Config.getNotifyUrl());

        //附加数据 查询API和支付通知中原样返回，可作为自定义参数使用，实际情况下只有支付完成状态才会返回该字段。
//        Map<String, String> attach = new HashMap<>();
//        attach.put("orderCode", out_trade_no);
//        obj.put("attach", JSON.toJSONString(attach));

        //订单金额
        JSONObject amount = new JSONObject();
//        amount.put("total", payRequestBody.getTotalFee());
//        amount.put("total", Integer.valueOf(df.format(Double.parseDouble(payRequestBody.getTotalFee()) * 100)));
        amount.put("total", Long.valueOf(df.format(totalPrice * 100))); //sysOrder中金额单位为元，此处需转成分。
        obj.put("amount", amount);

        //支付者
        JSONObject payer = new JSONObject();
        payer.put("openid", sysUser.getOpenId());
        obj.put("payer", payer);

        log.info("微信小程序支付JSAPI预下单请求参数:{}", obj.toJSONString());

        HttpPost httpPost = new HttpPost(wxPayV3Config.getJsApiUrl());
        httpPost.addHeader("Accept", "application/json");
        httpPost.addHeader("Content-type", "application/json; charset=utf-8");
        httpPost.setEntity(new StringEntity(obj.toJSONString(), "UTF-8"));

        String bodyAsString;
        try {
            if (httpClient == null) {
                log.info("微信小程序支付JSAPI预下单请求失败");
                return AjaxResult.error("预下单失败，请重试,无法连接微信支付服务器!");
            }
            //执行请求
            CloseableHttpResponse response = httpClient.execute(httpPost);
            bodyAsString = EntityUtils.toString(response.getEntity());
        } catch (IOException e) {
            log.info("微信小程序支付JSAPI预下单请求失败, err: {}", e.getMessage());
            return AjaxResult.error("预下单失败，请重试！ err: " + e.getMessage());
        }
        String prePayId = JSONObject.parseObject(bodyAsString).getString("prepay_id");
        if (prePayId == null) {
            String message = JSONObject.parseObject(bodyAsString).getString("message");
            log.info("微信小程序支付JSAPI预下单请求失败{}", message);
            return AjaxResult.error("预下单失败，请重试！" + message);
        }
        log.info("prePay prePayId: {}", prePayId);

        //准备小程序端的请求参数
//        resultMap.put("appId", wxPayV3Config.getAppId()); //这个参数不用下发给小程序端，其发起微信支付时无需携带此参数
        String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
        resultMap.put("timeStamp", timeStamp);
        String nonceStr = IdUtil.fastSimpleUUID();
        resultMap.put("nonceStr", nonceStr);
        String packageStr = "prepay_id=" + prePayId;
        resultMap.put("package", packageStr);
        resultMap.put("signType", "RSA");
        String signString = wxPayV3Config.getAppId() + "\n" + timeStamp + "\n" + nonceStr + "\n" + packageStr + "\n";
        resultMap.put("paySign", Base64.encode(sign.sign(signString.getBytes())));
        log.info("prePay resultMap: {}", resultMap);

        //开启定时器周期性查询订单支付状态
        //需先判断timerMap中是否已存在指定订单的支付状态查询计时器(比如当前调起支付的是之前未完成支付的订单)，如果已存在则无需重复创建。
//        if (EnvConfig.isDev()) {
        if (!payStatusQueryTimerMap.containsKey(orderId)) {
            Timer timer = new Timer();
            payStatusQueryTimerMap.put(orderId, timer);
            timer.schedule(new CheckPayStatusTask(orderId, orderType, payType), 3000, 3000);
        }
//        }

        return resultMap;
    }

    /**
     * 查询微信支付订单状态。
     *
     * @return
     */
    public Map<String, Object> queryPayStatus(String outOrderNo) {
        Map<String, Object> resultMap = new HashMap<>();

        //请求URL
        URIBuilder uriBuilder = null;
        try {
            uriBuilder = new URIBuilder(wxPayV3Config.getQueryOrder() + outOrderNo);
            uriBuilder.setParameter("mchid", wxPayV3Config.getMchId());
            if (httpClient == null) {
                return AjaxResult.error("查询订单失败，请重试,无法连接微信支付服务器!");
            }
            //完成签名并执行请求
            HttpGet httpGet = new HttpGet(uriBuilder.build());
            httpGet.addHeader("Accept", "application/json");
            CloseableHttpResponse response = httpClient.execute(httpGet);

            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200 || statusCode == 204) {
                String bodyAsString = EntityUtils.toString(response.getEntity());
                if (StringUtils.isNotEmpty(bodyAsString)) {
                    JSONObject res = JSON.parseObject(bodyAsString);
                    log.info("success,return body = " + bodyAsString);
                    //拿到商户订单号
                    String out_trade_no = res.getString("out_trade_no");
                    resultMap.put("out_trade_no", out_trade_no);
                    //交易状态，枚举值：
                    //SUCCESS：支付成功
                    //REFUND：转入退款
                    //NOTPAY：未支付
                    //CLOSED：已关闭
                    //REVOKED：已撤销（仅付款码支付会返回）
                    //USERPAYING：用户支付中（仅付款码支付会返回）
                    //PAYERROR：支付失败（仅付款码支付会返回）
                    //示例值：SUCCESS
                    String trade_state = res.getString("trade_state");
                    resultMap.put("trade_state", trade_state);
                    //交易状态描述
                    //示例值：支付成功
                    String trade_state_desc = res.getString("trade_state_desc");
                    resultMap.put("trade_state_desc", trade_state_desc);
                    switch (trade_state) {
                        case "SUCCESS":
                            JSONObject amountJson = res.getJSONObject("amount");
                            //订单总金额，单位为分。
                            Integer total = amountJson.getInteger("total");
                            //用户支付金额，单位为分。（指使用优惠券的情况下，这里等于总金额-优惠券金额）
                            Integer payerTotal = amountJson.getInteger("payer_total");
                            resultMap.put("total", total);
                            resultMap.put("payerTotal", payerTotal);

                            break;
                        case "REFUND":

                            break;
                    }
                }
            }
//            return AjaxResult.error("查询支付失败:" + "failed,resp code = " + statusCode + ",return body = " + EntityUtils.toString(response.getEntity()));
        } catch (Exception e) {
//            return AjaxResult.error("查询支付失败:" + e.getMessage());
        }

        return resultMap;
    }


    /**
     * 申请退款。微信支付申请退款API官方接口文档：https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_9.shtml
     *
     * @param reqBody
     * @return
     */
    @Override
    public Map<String, Object> refund(WxRefundReqBody reqBody) {
        //获取当前申请退款的订单id
        String orderId = reqBody.getOrderId();

        //准备微信支付退款接口需要的参数
        //商户订单编号
        String outOrderNo = "";
        //原订单已支付的总金额
        double total = 0;
        //申请退款金额
        double refund = 0;
        //生成商户退款订单编号
        String out_refund_no = IdUtil.simpleUUID();
        //退款原因
        String refundReason = reqBody.getReason();

        //获取当前退款的订单的类型。0：计时收费订单，1：套餐收费订单，2：拼团订单。
        int orderType = reqBody.getOrderType();
        //获取退款类型。0：只退押金，1：只退订单费用，2：退押金+订单费用。
        int refundType = reqBody.getRefundType();
        if (orderType == 0) { //计时收费订单
            SysJishiOrder sysJishiOrder = sysJishiOrderMapper.selectSysJishiOrderByOrderId(orderId);
            if (Objects.isNull(sysJishiOrder)) {
                throw new ServiceException(String.format("申请退款的计时收费订单[orderId:%s]不存在,无法退款.请检查订单号是否正确.", orderId));
            }
            if (refundType == 0) { //只退押金
                outOrderNo = sysJishiOrder.getDepositOutOrderNo();
                total = refund = sysJishiOrder.getRealPayDeposit(); //获取已实际支付的押金金额
                sysJishiOrder.setDepositPayStatus(PayStatus.Refunding.getCode()); //将押金支付状态修改为：退款中
                sysJishiOrder.setDepositPayStatusDesc(PayStatus.Refunding.getInfo());
                sysJishiOrder.setDepositOutRefundNo(out_refund_no);
            } else if (refundType == 1) { //只退订单费用
                outOrderNo = sysJishiOrder.getFeeOutOrderNo();
                total = refund = sysJishiOrder.getRealPayFee(); //获取已实际支付的订单费用金额
                sysJishiOrder.setFeePayStatus(PayStatus.Refunding.getCode()); //将订单费用支付状态修改为：退款中
                sysJishiOrder.setFeePayStatusDesc(PayStatus.Refunding.getInfo());
                sysJishiOrder.setFeeOutRefundNo(out_refund_no);
            } else if (refundType == 2) { //退押金+订单费用
                //无法同时退押金和费用，因为押金和费用在支付时是分两笔订单支付的，要退款也必须分两次
                throw new ServiceException("无法同时申请退押金和订单费用，押金和订单费用需分别申请退款.");
            } else {
                throw new ServiceException("退款类型参数refundType不符合要求，无法退款.");
            }

            sysJishiOrder.setRefundDesc(refundReason);
            sysJishiOrderMapper.updateSysJishiOrder(sysJishiOrder);

        } else if (orderType == 1) { //套餐收费订单
            SysTaocanOrder sysTaocanOrder = sysTaocanOrderMapper.selectSysTaocanOrderByOrderId(orderId);
            if (Objects.isNull(sysTaocanOrder)) {
                throw new ServiceException(String.format("申请退款的订单[orderId:%s]不存在,无法退款.请检查订单号是否正确.", orderId));
            }

            if (refundType == 0) { //只退押金
                outOrderNo = sysTaocanOrder.getDepositAndFeeOutOrderNo();
                total = sysTaocanOrder.getOrderFee() + sysTaocanOrder.getOrderDeposit(); //获取已实际支付的总金额(押金+订单费用)，注意，此处不能包含订单超时费用。
                refund = sysTaocanOrder.getRealPayDeposit(); //获取已实际支付的押金金额
                sysTaocanOrder.setDepositPayStatus(PayStatus.Refunding.getCode()); //将押金支付状态修改为：退款中
                sysTaocanOrder.setDepositPayStatusDesc(PayStatus.Refunding.getInfo());
                sysTaocanOrder.setDepositOutRefundNo(out_refund_no);
            } else if (refundType == 2) { //退押金与套餐费用
                outOrderNo = sysTaocanOrder.getDepositAndFeeOutOrderNo();
                total = sysTaocanOrder.getOrderFee() + sysTaocanOrder.getOrderDeposit(); //获取已实际支付的总金额(押金+订单费用)，注意，此处不能包含订单超时费用。
                refund = total; //设置需退款金额
                sysTaocanOrder.setDepositPayStatus(PayStatus.Refunding.getCode()); //将押金支付状态修改为：退款中
                sysTaocanOrder.setDepositPayStatusDesc(PayStatus.Refunding.getInfo());
                sysTaocanOrder.setFeePayStatus(PayStatus.Refunding.getCode()); //将套餐费用支付状态修改为：退款中
                sysTaocanOrder.setFeePayStatusDesc(PayStatus.Refunding.getInfo());
                sysTaocanOrder.setDepositAndFeeOutRefundNo(out_refund_no);
            } else if (refundType == 3) { //退超时费用
                outOrderNo = sysTaocanOrder.getOvertimeFeeOutOrderNo();
                total = sysTaocanOrder.getOvertimeFee(); //获取已实际支付的超时费用。
                refund = total; //设置需退款金额
                sysTaocanOrder.setOvertimeFeePayStatus(PayStatus.Refunding.getCode()); //将超时费用的支付状态修改为：退款中
                sysTaocanOrder.setOvertimeFeePayStatusDesc(PayStatus.Refunding.getInfo());
            } else { //套餐计费订单不存在其他类型的退费类型

            }

            sysTaocanOrderMapper.updateSysTaocanOrder(sysTaocanOrder);
        } else if (orderType == 2) { //拼团订单
            SysPintuanChildrenOrder sysPintuanChildrenOrder = sysPintuanChildrenOrderMapper.selectSysPintuanChildrenOrderByOrderId(orderId);
            if (Objects.isNull(sysPintuanChildrenOrder)) {
                throw new ServiceException(String.format("申请退款的订单[orderId:%s]不存在,无法退款.请检查订单id是否正确.", orderId));
            }

            if (refundType == 0) { //只退押金
                outOrderNo = sysPintuanChildrenOrder.getDepositAndFeeOutOrderNo();
                total = sysPintuanChildrenOrder.getOrderFee() + sysPintuanChildrenOrder.getOrderDeposit(); //获取已实际支付的总金额(押金+订单费用)，注意，此处不能包含订单超时费用。
                refund = sysPintuanChildrenOrder.getRealPayDeposit(); //获取已实际支付的押金金额
                sysPintuanChildrenOrder.setDepositPayStatus(PayStatus.Refunding.getCode()); //将押金支付状态修改为：退款中
                sysPintuanChildrenOrder.setDepositPayStatusDesc(PayStatus.Refunding.getInfo());
                sysPintuanChildrenOrder.setDepositOutRefundNo(out_refund_no);
            } else if (refundType == 2) { //退押金与套餐费用
                outOrderNo = sysPintuanChildrenOrder.getDepositAndFeeOutOrderNo();
                total = sysPintuanChildrenOrder.getOrderFee() + sysPintuanChildrenOrder.getOrderDeposit(); //获取已实际支付的总金额(押金+订单费用)，注意，此处不能包含订单超时费用。
                refund = total; //设置需退款金额
                sysPintuanChildrenOrder.setDepositPayStatus(PayStatus.Refunding.getCode()); //将押金支付状态修改为：退款中
                sysPintuanChildrenOrder.setDepositPayStatusDesc(PayStatus.Refunding.getInfo());
                sysPintuanChildrenOrder.setFeePayStatus(PayStatus.Refunding.getCode()); //将套餐费用支付状态修改为：退款中
                sysPintuanChildrenOrder.setFeePayStatusDesc(PayStatus.Refunding.getInfo());
                sysPintuanChildrenOrder.setDepositAndFeeOutRefundNo(out_refund_no);
            } else if (refundType == 3) { //退超时费用
                outOrderNo = sysPintuanChildrenOrder.getOvertimeFeeOutOrderNo();
                total = sysPintuanChildrenOrder.getOvertimeFee(); //获取已实际支付的超时费用。
                refund = total; //设置需退款金额
                sysPintuanChildrenOrder.setOvertimeFeePayStatus(PayStatus.Refunding.getCode()); //将超时费用的支付状态修改为：退款中
                sysPintuanChildrenOrder.setOvertimeFeePayStatusDesc(PayStatus.Refunding.getInfo());
            } else { //套餐计费订单不存在其他类型的退费类型

            }

            sysPintuanChildrenOrderMapper.updateSysPintuanChildrenOrder(sysPintuanChildrenOrder);
        } else {
            throw new ServiceException("退款订单类型参数orderType不符合要求，无法退款.");
        }

        //构建将发送给微信后端退款接口的参数对象
        JSONObject obj = new JSONObject();
        //商户订单号
        obj.put("out_trade_no", outOrderNo);
        //商户退款单号
        obj.put("out_refund_no", out_refund_no);
        //退款原因，非必填。
        obj.put("reason", refundReason);
        //退款结果回调url，非必填。
        obj.put("notify_url", wxPayV3Config.getNotifyUrl());

        //附加数据 查询API和支付通知中原样返回，可作为自定义参数使用，实际情况下只有支付完成状态才会返回该字段。

        //订单金额相关信息
        JSONObject amount = new JSONObject();
        //原订单金额(必填)，单位为分，只能为整数。
        amount.put("total", Long.valueOf(df.format(total * 100)));
        //退款金额(必填)，单位为分，只能为整数，不能超过原订单支付金额。示例值：888
        amount.put("refund", Long.valueOf(df.format(refund * 100)));
        //退款币种(必填)。符合ISO 4217标准的三位字母代码，目前只支持人民币：CNY。
        amount.put("currency", "CNY");
        obj.put("amount", amount);

        log.info("微信小程序支付JSAPI退款请求参数:{}", obj.toJSONString());

        HttpPost httpPost = new HttpPost(WxPayV3Url.Refund);
        httpPost.addHeader("Accept", "application/json");
        httpPost.addHeader("Content-type", "application/json; charset=utf-8");
        httpPost.setEntity(new StringEntity(obj.toJSONString(), "UTF-8"));

        String bodyAsString;
        try {
            if (httpClient == null) {
                log.info("微信小程序支付JSAPI退款请求失败");
                return AjaxResult.error("退款失败，请重试,无法连接微信支付服务器!");
            }
            //执行请求
            CloseableHttpResponse response = httpClient.execute(httpPost);
            bodyAsString = EntityUtils.toString(response.getEntity());
        } catch (IOException e) {
            log.info("微信小程序支付JSAPI预下单请求失败, err: {}", e.getMessage());
            return AjaxResult.error("预下单失败，请重试！ err: " + e.getMessage());
        }
        JSONObject resultObj = JSONObject.parseObject(bodyAsString);

        log.info("refund resultObj: {}", resultObj);

        Map<String, Object> resultMap = new HashMap<>();
        //获取错误码
        String errCode = resultObj.getString("code");

        if (StringUtils.isNotEmpty(errCode)) { //退款业务受理失败
            resultMap.put("errCode", errCode);
            resultMap.put("errMessage", resultObj.getString("message"));
            return resultMap; //直接返回
        }

        //退款到银行发现用户的卡作废或者冻结了，导致原路退款银行卡失败，可前往商户平台-交易中心，手动处理此笔退款。
        //枚举值：
        //SUCCESS：退款成功
        //CLOSED：退款关闭
        //PROCESSING：退款处理中
        //ABNORMAL：退款异常
        //示例值：SUCCESS
        resultMap.put("status", resultObj.getString("status"));
        resultMap.put("out_refund_no", resultObj.getString("out_refund_no"));
        //取当前退款单的退款入账方，有以下几种情况：
        //1）退回银行卡：{银行名称}{卡类型}{卡尾号}
        //2）退回支付用户零钱:支付用户零钱
        //3）退还商户:商户基本账户商户结算银行账户
        //4）退回支付用户零钱通:支付用户零钱通
        //示例值：招商银行信用卡0403
        resultMap.put("user_received_account", resultObj.getString("user_received_account"));
        //枚举值：
        //ORIGINAL：原路退款
        //BALANCE：退回到余额
        //OTHER_BALANCE：原账户异常退到其他余额账户
        //OTHER_BANKCARD：原银行卡异常退到其他银行卡
        //示例值：ORIGINAL
        resultMap.put("channel", resultObj.getString("channel"));

//        sysOrder.setWxRefundNo(resultObj.getString("refund_id")); //设置微信退款单号
//        //更新数据库中当前订单
//        sysOrderMapper.updateSysOrder(sysOrder);

        //开启定时器周期性查询订单退款状态
        if (!payStatusQueryTimerMap.containsKey(orderId)) {
            Timer timer = new Timer();
            payStatusQueryTimerMap.put(orderId, timer);
            timer.schedule(new CheckRefundStatusTask(orderId, orderType, refundType, out_refund_no), 3000, 3000);
        }

        return resultMap;
    }


    /**
     * 关闭订单。
     * <p>
     * 以下情况需要调用关单接口：
     * 1、商户订单支付失败需要生成新单号重新发起支付，要对原订单号调用关单，避免重复支付；
     * 2、系统下单后，用户支付超时，系统退出不再受理，避免用户继续，请调用关单接口。
     * <p>
     * 注意：
     * • 关单没有时间限制，建议在订单生成后间隔几分钟（最短5分钟）再调用关单接口，避免出现订单状态同步不及时导致关单失败。
     *
     * @param outTradeNo 商户系统内部订单号
     */
    public boolean closeOrder(String outTradeNo) {
        JSONObject obj = new JSONObject();
        //直连商户号
        obj.put("mchid", wxPayV3Config.getMchId());

        HttpPost httpPost = new HttpPost(StringUtils.replace(WxPayV3Url.CloseOrder, "{out_trade_no}", outTradeNo));
        httpPost.addHeader("Accept", "application/json");
        httpPost.addHeader("Content-type", "application/json; charset=utf-8");
        httpPost.setEntity(new StringEntity(obj.toJSONString(), "UTF-8"));

        String bodyAsString;
        try {
            if (httpClient == null) {
                throw new ServiceException("关闭订单失败，请重试,无法连接微信支付服务器!");
            }
            //执行请求
            CloseableHttpResponse response = httpClient.execute(httpPost);
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 204) { //关闭订单成功
                log.info("订单{}已被关闭，无法再进行支付.", outTradeNo);
                return true;
            }
//            bodyAsString = EntityUtils.toString(response.getEntity());
        } catch (IOException e) {
            log.error("closeOrder err: {}", e.getMessage());
        }

        return false;
    }

    /**
     * 客户端通知服务端用户已完成支付。
     *
     * @param outOrderNo
     * @param payType    订单类型。0：计时收费订单，1：套餐收费订单，2：拼团订单。
     */
//    @Override
//    public void completePay(String outOrderNo, int payType) {
//        SysOrder sysOrder = sysOrderMapper.selectSysOrderByOutOrderNo(outOrderNo);
//
//        //更新数据库中当前订单的信息
//        if (payType == 0) { //仅支付押金
//            sysOrder.setDepositPayStatus(PayStatus.Paying.getCode());
//            sysOrder.setDepositPayStatusDesc(PayStatus.Paying.getInfo());
//        } else if (payType == 1) { //仅支付订单费用
//            sysOrder.setFeePayStatus(PayStatus.Paying.getCode());
//            sysOrder.setFeePayStatusDesc(PayStatus.Paying.getInfo());
//        } else if (payType == 2) { //支付押金+费用
//            sysOrder.setDepositPayStatus(PayStatus.Paying.getCode());
//            sysOrder.setDepositPayStatusDesc(PayStatus.Paying.getInfo());
//            sysOrder.setFeePayStatus(PayStatus.Paying.getCode());
//            sysOrder.setFeePayStatusDesc(PayStatus.Paying.getInfo());
//        } else {
//            throw new ServiceException("支付类型参数payType不符合要求");
//        }
//
//        sysOrderMapper.updateSysOrder(sysOrder);
//    }

    /**
     * 查询退款状态。
     *
     * @param outRefundNo
     * @return
     */
    @Override
    public Map<String, Object> queryRefundStatus(String outRefundNo) {
        Map<String, Object> resultMap = new HashMap<>();
        //请求URL
        URIBuilder uriBuilder = null;
        try {
            uriBuilder = new URIBuilder(WxPayV3Url.QueryRefundStatus + outRefundNo);
            if (httpClient == null) {
                return AjaxResult.error("查询退款状态失败，请重试,无法连接微信支付服务器!");
            }
            //完成签名并执行请求
            HttpGet httpGet = new HttpGet(uriBuilder.build());
            httpGet.addHeader("Accept", "application/json");
            CloseableHttpResponse response = httpClient.execute(httpGet);

            log.info("queryRefundStatus response: {}", response);

            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200 || statusCode == 204) {
                String bodyAsString = EntityUtils.toString(response.getEntity());
                if (StringUtils.isNotEmpty(bodyAsString)) {
                    JSONObject resultObj = JSON.parseObject(bodyAsString);
                    log.info("queryRefundStatus resultObj: {}", resultObj);

                    //商户退款单号
                    resultMap.put("out_refund_no", resultObj.getString("out_refund_no"));
                    //商户订单号
                    resultMap.put("out_trade_no", resultObj.getString("out_trade_no"));
                    //退款到银行发现用户的卡作废或者冻结了，导致原路退款银行卡失败，可前往商户平台-交易中心，手动处理此笔退款。
                    //枚举值：
                    //SUCCESS：退款成功
                    //CLOSED：退款关闭
                    //PROCESSING：退款处理中
                    //ABNORMAL：退款异常
                    //示例值：SUCCESS
                    resultMap.put("status", resultObj.getString("status"));
                    //取当前退款单的退款入账方，有以下几种情况：
                    //1）退回银行卡：{银行名称}{卡类型}{卡尾号}
                    //2）退回支付用户零钱:支付用户零钱
                    //3）退还商户:商户基本账户商户结算银行账户
                    //4）退回支付用户零钱通:支付用户零钱通
                    //示例值：招商银行信用卡0403
                    resultMap.put("user_received_account", resultObj.getString("user_received_account"));
                    //枚举值：
                    //ORIGINAL：原路退款
                    //BALANCE：退回到余额
                    //OTHER_BALANCE：原账户异常退到其他余额账户
                    //OTHER_BANKCARD：原银行卡异常退到其他银行卡
                    //示例值：ORIGINAL
                    resultMap.put("channel", resultObj.getString("channel"));
                    //退款成功时间
                    resultMap.put("success_time", resultObj.getString("success_time"));
                    //退款受理时间
                    //示例值：2020-12-01T16:18:12+08:00
                    resultMap.put("create_time", resultObj.getString("create_time"));

                    //获取金额详细信息
                    JSONObject amountJson = resultObj.getJSONObject("amount");
                    //订单总金额，单位为分。
                    Integer total = amountJson.getInteger("total");
                    //退款标价金额，单位为分，可以做部分退款
                    Integer refund = amountJson.getInteger("refund");
                    resultMap.put("total", total);
                    resultMap.put("refund", refund);
                }
            }
        } catch (Exception e) {
            log.error("queryRefundStatus err: {}", e.getMessage());
        }

        return resultMap;
    }

    /**
     * 根据商户订单编号停止指定的支付状态检查定时器。
     *
     * @param orderId
     */
    @Override
    public void stopCheckPayStatusTimer(String orderId) {
        Timer timer = payStatusQueryTimerMap.get(orderId);
        if (Objects.nonNull(timer)) {
            timer.cancel();
            payStatusQueryTimerMap.remove(orderId);
        }
    }

    /**
     * 支付成功后的处理。
     *
     * @param outOrderNo 实际支付成功的金额。
     * @param payerTotal 实际支付成功的金额。
     */

    /**
     * 支付成功后的处理方法。
     *
     * @param outOrderNo 商户订单号
     * @param payerTotal 实际支付成功的金额
     * @param orderId    订单id
     * @param orderType  订单类型。0：计时收费订单，1：套餐收费订单，2：拼团订单。
     * @param payType    支付类型。0：仅支付押金，1：仅支付订单费用，2：支付押金+费用
     */
    @Override
    public void executeAfterPaySucceed(String outOrderNo, int payerTotal, String orderId, int orderType, int payType) {
        log.info("executeAfterPaySucceed outOrderNo:{}, payerTotal: {}, orderId: {}, orderType: {}, payType: {}",
                outOrderNo, payerTotal, orderId, orderType, payType);

        if (orderType == 0) {
            //计时计费订单
            //根据orderId查询对应的计时计费订单
            SysJishiOrder sysJishiOrder = sysJishiOrderMapper.selectSysJishiOrderByOrderId(orderId);
            //先判断当前订单的支付类型。支付类型。0：仅支付押金，1：仅支付订单费用，2：支付押金+费用
            if (payType == 0) { //仅支付押金
                //更新订单状态为"待使用"。用户此时已支付了计时计费订单的押金，但还未扫码确认上船。
                sysJishiOrder.setOrderStatus(OrderStatus.ToUse.getCode());
                sysJishiOrder.setOrderStatusDesc(OrderStatus.ToUse.getInfo());

                //更新押金支付状态为“已支付”
                sysJishiOrder.setDepositPayStatus(PayStatus.Paid.getCode());
                sysJishiOrder.setDepositPayStatusDesc(PayStatus.Paid.getInfo());
                double realPrice = (new BigDecimal(payerTotal)).divide(new BigDecimal(100), 2, BigDecimal.ROUND_UP).doubleValue();
                //设置押金实际支付金额 (注意：此处需要将单位从分转为元，且数据格式为double类型，并保留2位小数)
                sysJishiOrder.setRealPayDeposit(realPrice);
                //设置当前订单用户已实际支付的总金额(此时总金额中只有当前支付的押金金额) (注意：此处需要将单位从分转为元，且数据格式为double类型，并保留2位小数)
                sysJishiOrder.setRealPayTotalPrice(realPrice);

                //更新数据库中当前计时计费订单的信息
                sysJishiOrderMapper.updateSysJishiOrder(sysJishiOrder);

                //更新船只与订单信息关联表中订单的状态
                UserBoatOrderInfo userBoatOrderInfo = new UserBoatOrderInfo();
                userBoatOrderInfo.setUserId(sysJishiOrder.getUserId());
                userBoatOrderInfo.setOrderStatus(OrderStatus.ToUse.getCode());
                userBoatOrderInfo.setOrderStatusDesc(OrderStatus.ToUse.getInfo());
                userBoatOrderInfoService.updateBoatOrderInfo(userBoatOrderInfo);

                //更新船只状态为“上船倒计时中”
                Long boatId = sysJishiOrder.getBoatId();
                sysBoatService.updateBoatStatusByBoadId(boatId, BoatStatus.OnBoatCounting);

                //主动将redis缓存中与当前订单支付相关的key删除
                CacheUtils.remove(CacheConstants.Order_Pay_Status_Redis_Key, orderId + ":" + orderType);

                //利用redis实现开启扫码确认上船倒计时
                // 新创建的计时计费订单在押金支付成功后就会更新为待使用状态，且同时开启上船倒计时，如果超过指定时限仍未扫码确认上船则系统会关闭当前订单，并主动给用户退款。
                CacheUtils.put(CacheConstants.Order_Confirm_On_Boat_Status_Redis_Key, orderId + ":" + orderType, "", onBoatCountdownDuration, TimeUnit.SECONDS);
            } else if (payType == 1) { //仅支付订单费用
                //更新订单状态为"已完成"。用户此时已支付了计时计费订单的费用(押金在之前已支付)，说明此时游玩已结束，船已停靠到码头，且船锁已成功上锁。
                sysJishiOrder.setOrderStatus(OrderStatus.Fulfilled.getCode());
                sysJishiOrder.setOrderStatusDesc(OrderStatus.Fulfilled.getInfo());

                //更新订单费用支付状态为“已支付”
                sysJishiOrder.setFeePayStatus(PayStatus.Paid.getCode());
                sysJishiOrder.setFeePayStatusDesc(PayStatus.Paid.getInfo());
                //设置订单费用实际支付金额 (注意：此处需要将单位从分转为元，且数据格式为double类型，并保留2位小数)
                double realPayFee = (new BigDecimal(payerTotal)).divide(new BigDecimal(100), 2, BigDecimal.ROUND_UP).doubleValue();
                sysJishiOrder.setRealPayFee(realPayFee);
                //设置当前订单用户已实际支付的总金额(当前的订单费用+之前已支付的押金金额)
                sysJishiOrder.setRealPayTotalPrice(realPayFee + sysJishiOrder.getRealPayDeposit());

                //设置订单关闭时间。此时的计时计费订单才算真正完成，可以关闭了。
                sysJishiOrder.setClosedTime(new Date());
                sysJishiOrder.setRemark("订单费用支付成功，订单已完成");

                //更新数据库中当前计时计费订单的信息
                sysJishiOrderMapper.updateSysJishiOrder(sysJishiOrder);

                //删除船只订单关联对象
                userBoatOrderInfoService.deleteUserBoatOrderInfoByUserId(sysJishiOrder.getUserId());

                //订单已完成，退还押金
                new Timer().schedule(new TimerTask() {
                    @Override
                    public void run() {
                        refund(new WxRefundReqBody(orderId, 0, 0, "订单已完成，退还押金"));
                    }
                }, 5000);

                return;
            } else if (payType == 2) {
                //计时计费订单不存在同时支付押金和订单费用的情况
            }
        } else if (orderType == 1) {
            //套餐付费订单
            SysTaocanOrder sysTaocanOrder = sysTaocanOrderMapper.selectSysTaocanOrderByOrderId(orderId);
            //先判断当前订单的支付类型。支付类型。0：仅支付押金，1：仅支付订单费用，2：支付押金+费用
            if (payType == 2) { //同时支付押金+费用的情况
                //更新订单状态为"待使用"。用户此时已支付了押金和费用，但还未确认上船，因此订单状态为待使用。
                sysTaocanOrder.setOrderStatus(OrderStatus.ToUse.getCode());
                sysTaocanOrder.setOrderStatusDesc(OrderStatus.ToUse.getInfo());

                //更新押金支付状态为“已支付”
                sysTaocanOrder.setDepositPayStatus(PayStatus.Paid.getCode());
                sysTaocanOrder.setDepositPayStatusDesc(PayStatus.Paid.getInfo());
                //设置押金实际支付金额
                sysTaocanOrder.setRealPayDeposit(sysTaocanOrder.getOrderDeposit());
                //更新订单费用支付状态为“已支付”
                sysTaocanOrder.setFeePayStatus(PayStatus.Paid.getCode());
                sysTaocanOrder.setFeePayStatusDesc(PayStatus.Paid.getInfo());
                //设置订单费用实际支付金额
                sysTaocanOrder.setRealPayFee(sysTaocanOrder.getOrderFee());
                //设置当前订单用户已实际支付的总金额(当前的订单费用+之前已支付的押金金额) (注意：此处需要将单位从分转为元，且数据格式为double类型，并保留2位小数)
                double realPayTotalPrice = (new BigDecimal(payerTotal)).divide(new BigDecimal(100), 2, BigDecimal.ROUND_UP).doubleValue();
                sysTaocanOrder.setRealPayTotalPrice(realPayTotalPrice);

                //更新数据库中当前订单的信息
                sysTaocanOrderMapper.updateSysTaocanOrder(sysTaocanOrder);

                //更新船只与订单信息关联表中订单的状态
                UserBoatOrderInfo userBoatOrderInfo = new UserBoatOrderInfo();
                userBoatOrderInfo.setUserId(sysTaocanOrder.getUserId());
                userBoatOrderInfo.setOrderStatus(OrderStatus.ToUse.getCode());
                userBoatOrderInfo.setOrderStatusDesc(OrderStatus.ToUse.getInfo());
                userBoatOrderInfoService.updateBoatOrderInfo(userBoatOrderInfo);

                //更新船只状态为“上船倒计时中”
                Long boatId = sysTaocanOrder.getBoatId();
                sysBoatService.updateBoatStatusByBoadId(boatId, BoatStatus.OnBoatCounting);

                //主动将redis缓存中与当前订单支付相关的key删除
                CacheUtils.remove(CacheConstants.Order_Pay_Status_Redis_Key, orderId + ":" + orderType);
                //利用redis实现开启扫码确认上船倒计时
                // 新创建的计时计费订单在押金支付成功后就会更新为待使用状态，且同时开启上船倒计时，如果超过指定时限仍未扫码确认上船则系统会关闭当前订单，并主动给用户退款。
                CacheUtils.put(CacheConstants.Order_Confirm_On_Boat_Status_Redis_Key, orderId + ":" + orderType, "", onBoatCountdownDuration, TimeUnit.SECONDS);
            } else if (payType == 3) {  //支付超时费用
                //更新订单状态为"已完成"。用户此时已支付了计时计费订单的费用(押金在之前已支付)，说明此时游玩已结束，船已停靠到码头，且船锁已成功上锁。
                sysTaocanOrder.setOrderStatus(OrderStatus.Fulfilled.getCode());
                sysTaocanOrder.setOrderStatusDesc(OrderStatus.Fulfilled.getInfo());

                //更新订单超时费用支付状态为“已支付”
                sysTaocanOrder.setOvertimeFeePayStatus(PayStatus.Paid.getCode());
                sysTaocanOrder.setOvertimeFeePayStatusDesc(PayStatus.Paid.getInfo());
                //设置订单超时费用实际支付金额 (注意：此处需要将单位从分转为元，且数据格式为double类型，并保留2位小数)
                double realPayOvertimeFee = (new BigDecimal(payerTotal)).divide(new BigDecimal(100), 2, BigDecimal.ROUND_UP).doubleValue();
                sysTaocanOrder.setRealPayOvertimeFee(realPayOvertimeFee);
                //设置当前订单用户已实际支付的总金额(当前已支付的押金+套餐费用+超时费用)
                sysTaocanOrder.setRealPayTotalPrice(sysTaocanOrder.getRealPayDeposit() + sysTaocanOrder.getRealPayFee() + realPayOvertimeFee);

                //设置订单关闭时间。此时的计时计费订单才算真正完成，可以关闭了。
                sysTaocanOrder.setClosedTime(new Date());
                sysTaocanOrder.setRemark("订单费用支付成功，订单已完成");

                //将船只状态更新为“空闲”
                SysBoat sysBoat = new SysBoat();
                sysBoat.setBoatId(sysTaocanOrder.getBoatId());
                sysBoat.setStatus(BoatStatus.Idle.getCode());
                sysBoat.setStatusDesc(BoatStatus.Idle.getInfo());
                sysBoatMapper.updateSysBoat(sysBoat);

                //订单已完成，退还押金
                new Timer().schedule(new TimerTask() {
                    @Override
                    public void run() {
                        refund(new WxRefundReqBody(orderId, 1, 0, "订单已完成，退还押金"));
                    }
                }, 5000);

                //更新数据库中当前订单的信息
                sysTaocanOrderMapper.updateSysTaocanOrder(sysTaocanOrder);

                //删除船只订单关联对象
                userBoatOrderInfoService.deleteUserBoatOrderInfoByUserId(sysTaocanOrder.getUserId());
            } else {
                log.error("payType:{}不符合要求，套餐付费订单只存在同时支付押金+费用的情况，即payType只能为2。", payType);
            }
        } else if (orderType == 2) { //拼团订单
            SysPintuanChildrenOrder sysPintuanChildrenOrder = sysPintuanChildrenOrderMapper.selectSysPintuanChildrenOrderByOrderId(orderId);
            Long boatId = sysPintuanChildrenOrder.getBoatId();
            SysBoat sysBoat = sysBoatMapper.selectSysBoatByBoatId(boatId);

            Long userId = sysPintuanChildrenOrder.getUserId();

            //先判断当前订单的支付类型。支付类型。0：仅支付押金，1：仅支付订单费用，2：支付押金+费用
            if (payType == 2) { //支付押金+费用
                //删除支付倒计时
                CacheUtils.remove(CacheConstants.Order_Pay_Status_Redis_Key, orderId + ":" + orderType);

                //更新当前子订单的状态为"待使用"
                sysPintuanChildrenOrder.setOrderStatus(OrderStatus.ToUse.getCode()); //更新订单当前状态为待使用(不管此时拼团是否成功，此处都应更新为待使用)
                sysPintuanChildrenOrder.setOrderStatusDesc(OrderStatus.ToUse.getInfo());

                //更新押金支付状态为“已支付”
                sysPintuanChildrenOrder.setDepositPayStatus(PayStatus.Paid.getCode());
                sysPintuanChildrenOrder.setDepositPayStatusDesc(PayStatus.Paid.getInfo());
                //设置押金实际支付金额
                sysPintuanChildrenOrder.setRealPayDeposit(sysPintuanChildrenOrder.getOrderDeposit());

                //更新订单费用支付状态为“已支付”
                sysPintuanChildrenOrder.setFeePayStatus(PayStatus.Paid.getCode());
                sysPintuanChildrenOrder.setFeePayStatusDesc(PayStatus.Paid.getInfo());
                //设置订单费用实际支付金额
                sysPintuanChildrenOrder.setRealPayFee(sysPintuanChildrenOrder.getOrderFee());

                //设置当前订单用户已实际支付的总金额(当前的订单费用+之前已支付的押金金额) (注意：此处需要将单位从分转为元，且数据格式为double类型，并保留2位小数)
                double realPayTotalPrice = (new BigDecimal(payerTotal)).divide(new BigDecimal(100), 2, BigDecimal.ROUND_UP).doubleValue();
                sysPintuanChildrenOrder.setRealPayTotalPrice(realPayTotalPrice);

                //先判断当前用户是否是当前拼团订单的首个创建者
                long expireTime = CacheUtils.getExpire(CacheConstants.Pintuan_Order_Redis_Key, String.valueOf(boatId));
                SysPintuanParentOrder sysPintuanParentOrder;
                if (expireTime <= 0) {
                    //指定boatId的船只当前未在拼团中，因此当前用户是此拼团订单的发起人

                    //1，创建并初始化拼团父订单对象SysPintuanParentOrder
                    sysPintuanParentOrder = new SysPintuanParentOrder();
                    BeanUtils.copyProperties(sysPintuanChildrenOrder, sysPintuanParentOrder);
                    //设置父订单id
                    String parentOrderId = ShortIdUtils.generateOrderIdStr();
                    sysPintuanParentOrder.setOrderId(parentOrderId);
                    //设置实际参与拼团人数
                    sysPintuanParentOrder.setPintuanPersonNum(sysPintuanChildrenOrder.getPintuanPersonNum());

                    //2，设置当前子订单的父订单id
                    sysPintuanChildrenOrder.setParentOrderId(parentOrderId);

                    //根据用户id查询对应的用户信息。
                    SysUser sysUser = sysUserService.selectUserById(userId);

                    //3，创建并初始化船只订单关联表
                    UserBoatOrderInfo userBoatOrderInfo = new UserBoatOrderInfo();
                    userBoatOrderInfo.setUserId(userId);
                    if (Objects.nonNull(sysUser)) {
                        userBoatOrderInfo.setUserPhone(sysUser.getPhonenumber());
                    }
                    userBoatOrderInfo.setBoatId(boatId);
                    userBoatOrderInfo.setBoatIdentityId(sysBoat.getBoatIdentityId());
                    userBoatOrderInfo.setOrderId(parentOrderId); //此处设置的是父订单id
                    userBoatOrderInfo.setOrderStatus(OrderStatus.ToUse.getCode()); //设置拼团订单当前状态为待使用
                    userBoatOrderInfo.setOrderType(2); //设置订单类型为拼团订单
                    userBoatOrderInfoService.insertBoatOrderInfo(userBoatOrderInfo);

                    //判断当前请求拼团人数是否等于当前拼团类型允许的最大拼团人数(此处不会存在大于的情况，因为在之前创建当前子订单时会判断此种情况，如果大于会抛出异常)
                    if (Objects.equals(sysPintuanChildrenOrder.getPintuanPersonNum(), sysPintuanChildrenOrder.getPintuanAllowPersonNum())) {
                        //此时已拼团成功，无需开启拼团倒计时，直接通知当前用户拼团成功
                        Channel miniProgramChannel = WebSocketChannelManager.getMiniprogramWebsocketChannelByUserId(userId);
                        if (Objects.nonNull(miniProgramChannel)) {
                            PintuanStatusMsg pintuanStatusMsg = new PintuanStatusMsg();
                            pintuanStatusMsg.setBoatId(boatId);
                            pintuanStatusMsg.setStatus(2); //设置拼团状态。0: 拼团中，1：拼团失败。2：拼团成功。
                            WebSocketMessageHandler.notifyClientSidePintuanStatusModified(miniProgramChannel, pintuanStatusMsg);
                        }

                        log.info("拼团成功！拼团父订单id：{}", parentOrderId);

                        //将子订单的拼团状态设置为“拼团成功”
                        sysPintuanChildrenOrder.setPintuanStatus(PintuanStatus.GroupBookingSucceed.getCode());
                        sysPintuanChildrenOrder.setPintuanStatusDesc(PintuanStatus.GroupBookingSucceed.getInfo());

                        //将父订单的拼团状态设置为“拼团成功”
                        sysPintuanParentOrder.setPintuanStatus(PintuanStatus.GroupBookingSucceed.getCode());
                        sysPintuanParentOrder.setPintuanStatusDesc(PintuanStatus.GroupBookingSucceed.getInfo());

                        //将船只状态从"空闲"更新为“上船倒计时中”
                        sysBoat.setStatus(BoatStatus.OnBoatCounting.getCode());
                        sysBoat.setStatusDesc(BoatStatus.OnBoatCounting.getInfo());

                        //拼团成功后，还需开启上船倒计时
                        CacheUtils.put(CacheConstants.Order_Confirm_On_Boat_Status_Redis_Key, parentOrderId + ":" + orderType, "", onBoatCountdownDuration, TimeUnit.SECONDS);
                    } else {
                        //此时拼团未成功，需开启拼团倒计时
                        CacheUtils.put(CacheConstants.Pintuan_Order_Redis_Key, String.valueOf(boatId), "", pintuanCountdownDuration, TimeUnit.SECONDS);

                        //将船只状态从"空闲"更新为“拼团中”
                        sysBoat.setStatus(BoatStatus.GroupBooking.getCode());
                        sysBoat.setStatusDesc(BoatStatus.GroupBooking.getInfo());

                        //将子订单的拼团状态设置为“拼团中”
                        sysPintuanChildrenOrder.setPintuanStatus(PintuanStatus.GroupBooking.getCode());
                        sysPintuanChildrenOrder.setPintuanStatusDesc(PintuanStatus.GroupBooking.getInfo());

                        //将父订单的拼团状态设置为“拼团中”
                        sysPintuanParentOrder.setPintuanStatus(PintuanStatus.GroupBooking.getCode());
                        sysPintuanParentOrder.setPintuanStatusDesc(PintuanStatus.GroupBooking.getInfo());

                        log.info("开启新的拼团，拼团父订单id：{}", parentOrderId);
                    }

                    //设置父订单的状态为"待使用"。拼团父订单在订单开始计费前的状态都为"待使用".
                    sysPintuanParentOrder.setOrderStatus(OrderStatus.ToUse.getCode());
                    sysPintuanParentOrder.setOrderStatusDesc(OrderStatus.ToUse.getInfo());

                    //将sysPintuanParentOrder插入数据库
                    sysPintuanParentOrderMapper.insertSysPintuanParentOrder(sysPintuanParentOrder);
                } else {
                    //当前船只正在拼团中，当前用户非此拼团订单的发起人
                    sysPintuanParentOrder = sysPintuanParentOrderMapper.selectSysPintuanParentOrderByOrderId(sysPintuanChildrenOrder.getParentOrderId());
                    List<SysPintuanChildrenOrder> pintuanChildrenOrderList = sysPintuanParentOrder.getPintuanChildrenOrderList();
                    //先判断当前拼团是否成功
                    int pintuanAllowPersonNum = sysPintuanParentOrder.getPintuanAllowPersonNum();
                    //注意：如下判断基本可认为不存在大于的情况，因为在用户请求支付拼团订单的押金与费用前会先检查下当时请求拼团的人数是否已超过允许的最大人数
                    if (sysPintuanChildrenOrder.getPintuanPersonNum() + sysPintuanParentOrder.getPintuanPersonNum() == pintuanAllowPersonNum) {
                        //此时已拼团成功，需关闭拼团倒计时，并通知所有参与拼团的用户拼团成功
                        log.info("拼团成功！拼团父订单id：{}", sysPintuanParentOrder.getOrderId());

                        //将子订单的拼团状态设置为“拼团成功”
                        sysPintuanChildrenOrder.setPintuanStatus(PintuanStatus.GroupBookingSucceed.getCode());
                        sysPintuanChildrenOrder.setPintuanStatusDesc(PintuanStatus.GroupBookingSucceed.getInfo());

                        //将父订单的拼团状态设置为“拼团成功”
                        sysPintuanParentOrder.setPintuanStatus(PintuanStatus.GroupBookingSucceed.getCode());
                        sysPintuanParentOrder.setPintuanStatusDesc(PintuanStatus.GroupBookingSucceed.getInfo());

                        //关闭拼团倒计时
                        CacheUtils.remove(CacheConstants.Pintuan_Order_Redis_Key, String.valueOf(sysPintuanParentOrder.getBoatId()));

                        //通知所有参与拼团的用户拼团成功了
                        pintuanChildrenOrderList.forEach(childrenOrder -> {
                            Channel miniProgramChannel = WebSocketChannelManager.getMiniprogramWebsocketChannelByUserId(userId);
                            if (Objects.nonNull(miniProgramChannel)) {
                                PintuanStatusMsg pintuanStatusMsg = new PintuanStatusMsg();
                                pintuanStatusMsg.setBoatId(childrenOrder.getBoatId());
                                pintuanStatusMsg.setStatus(2); //设置拼团状态。0: 拼团中，1：拼团失败。2：拼团成功。
                                WebSocketMessageHandler.notifyClientSidePintuanStatusModified(miniProgramChannel, pintuanStatusMsg);
                            }
                        });

                        //拼团成功后，还需开启上船倒计时
                        CacheUtils.put(CacheConstants.Order_Confirm_On_Boat_Status_Redis_Key, sysPintuanParentOrder.getOrderId() + ":" + orderType, "", 60, TimeUnit.SECONDS);

                        //将船只状态从"拼团中"更新为“上船倒计时中”
                        sysBoat.setStatus(BoatStatus.OnBoatCounting.getCode());
                        sysBoat.setStatusDesc(BoatStatus.OnBoatCounting.getInfo());
                    } else {
                        //此时拼团还未成功，还需继续拼团

                        //将订单的拼团状态设置为“拼团中”，父订单的拼团状态此时无需修改。
                        sysPintuanChildrenOrder.setPintuanStatus(PintuanStatus.GroupBooking.getCode());
                        sysPintuanChildrenOrder.setPintuanStatusDesc(PintuanStatus.GroupBooking.getInfo());
                    }

                    //设置当前最新的实际参与拼团人数
                    sysPintuanParentOrder.setPintuanPersonNum(sysPintuanChildrenOrder.getPintuanPersonNum() + sysPintuanParentOrder.getPintuanPersonNum());
                    //更新数据库中的父订单对象
                    sysPintuanParentOrderMapper.updateSysPintuanParentOrder(sysPintuanParentOrder);
                }

                //给子订单设置父订单id
                sysPintuanChildrenOrder.setParentOrderId(sysPintuanParentOrder.getOrderId());
                //更新数据库中的当前子订单对象
                sysPintuanChildrenOrderMapper.updateSysPintuanChildrenOrder(sysPintuanChildrenOrder);

                //更新船只状态
                sysBoatMapper.updateSysBoat(sysBoat);
            } else if (payType == 3) { //支付超时费用
                //更新订单状态为"已完成"。用户此时已支付了计时计费订单的费用(押金在之前已支付)，说明此时游玩已结束，船已停靠到码头，且船锁已成功上锁。
                sysPintuanChildrenOrder.setOrderStatus(OrderStatus.Fulfilled.getCode());
                sysPintuanChildrenOrder.setOrderStatusDesc(OrderStatus.Fulfilled.getInfo());

                //更新订单超时费用支付状态为“已支付”
                sysPintuanChildrenOrder.setOvertimeFeePayStatus(PayStatus.Paid.getCode());
                sysPintuanChildrenOrder.setOvertimeFeePayStatusDesc(PayStatus.Paid.getInfo());
                //设置订单超时费用实际支付金额 (注意：此处需要将单位从分转为元，且数据格式为double类型，并保留2位小数)
                double realPayOvertimeFee = (new BigDecimal(payerTotal)).divide(new BigDecimal(100), 2, BigDecimal.ROUND_UP).doubleValue();
                sysPintuanChildrenOrder.setRealPayOvertimeFee(realPayOvertimeFee);
                //设置当前订单用户已实际支付的总金额(当前已支付的押金+套餐费用+超时费用)
                sysPintuanChildrenOrder.setRealPayTotalPrice(sysPintuanChildrenOrder.getRealPayDeposit() + sysPintuanChildrenOrder.getRealPayFee() + realPayOvertimeFee);

                //设置订单关闭时间。此时的计时计费订单才算真正完成，可以关闭了。
                sysPintuanChildrenOrder.setClosedTime(new Date());
                sysPintuanChildrenOrder.setRemark("订单费用支付成功，订单已完成");

                //订单已完成，退还押金
                new Timer().schedule(new TimerTask() {
                    @Override
                    public void run() {
                        refund(new WxRefundReqBody(orderId, 2, 0, "订单已完成，退还押金"));
                    }
                }, 5000);

                //更新数据库中当前订单的信息
                sysPintuanChildrenOrderMapper.updateSysPintuanChildrenOrder(sysPintuanChildrenOrder);

                //检查是否所有已扫码上船的用户都已完成并关闭订单
                SysPintuanParentOrder sysPintuanParentOrder = sysPintuanParentOrderMapper.selectSysPintuanParentOrderByOrderId(sysPintuanChildrenOrder.getParentOrderId());
                if (Objects.isNull(sysPintuanParentOrder)) return;
                List<SysPintuanChildrenOrder> pintuanChildrenOrderList = sysPintuanParentOrder.getPintuanChildrenOrderList();

                boolean notAllOrderFulfilled = false;
                for (SysPintuanChildrenOrder childrenOrder1 : pintuanChildrenOrderList) {
                    if (childrenOrder1.getOnBoatStatus() == 1 && childrenOrder1.getOrderStatus() != OrderStatus.Fulfilled.getCode()) {
                        //此时说明还有已上船的子订单未完成
                        notAllOrderFulfilled = true;
                        break;
                    }
                }

                if (!notAllOrderFulfilled) { //所有已扫码上船的用户都已完成并关闭订单
                    sysPintuanParentOrder.setOrderStatus(OrderStatus.Fulfilled.getCode());
                    sysPintuanParentOrder.setOrderStatusDesc(OrderStatus.Fulfilled.getInfo());
                    sysPintuanParentOrderMapper.updateSysPintuanParentOrder(sysPintuanParentOrder);

                    //删除船只订单关联对象
                    userBoatOrderInfoService.deleteUserBoatOrderInfoByUserId(sysPintuanParentOrder.getUserId());
                }

            } else {
                log.error("payType:{}不符合要求，拼团订单只存在同时支付押金+费用的情况，即payType只能为2。", payType);
            }
        }
    }

    /**
     * 根据订单id取消该订单id对应的支付状态查询定时器。
     *
     * @param orderId
     */
    public void cancelPayStatusQueryTimer(String orderId) {
        Timer timer = payStatusQueryTimerMap.get(orderId);
        if (Objects.nonNull(timer)) {
            timer.cancel();
            payStatusQueryTimerMap.remove(orderId);
        }
    }


    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class WxPayAttachObject implements Serializable {
        private static final long serialVersionUID = 1L;
        /**
         * 订单id。
         */
        private String orderId;
        /**
         * 订单类型。0：计时收费订单，1：套餐收费订单，2：拼团订单。
         */
        private int orderType;
        /**
         * 支付类型。0：仅支付押金，1：仅支付订单费用，2：支付押金+费用，3：支付超时费用
         */
        private int payType;
    }

    /**
     * 查询支付状态的定时任务。
     */
    class CheckPayStatusTask extends TimerTask {

        /**
         * 订单id。
         */
        private String orderId;
        /**
         * 订单类型。0：计时收费订单，1：套餐收费订单，2：拼团订单。
         */
        private int orderType;
        /**
         * 支付类型。0：仅支付押金，1：仅支付订单费用，2：支付押金+费用，3：支付超时费用
         */
        private int payType;
        private Timer timer;

        public CheckPayStatusTask(String orderId, int orderType, int payType) {
            this.orderId = orderId;
            this.orderType = orderType;
            this.payType = payType;
        }

        @Override
        public void run() {
            if (timer == null) {
                timer = payStatusQueryTimerMap.get(orderId);
            }

            BaseOrder baseOrder = null;

            //获取当前订单的商户订单编号
            String outOrderNo = "";
            if (orderType == 0) { //计时计费订单
                SysJishiOrder sysJishiOrder = sysJishiOrderMapper.selectSysJishiOrderByOrderId(orderId);
                if (Objects.isNull(sysJishiOrder)) return;
                baseOrder = sysJishiOrder;
                if (payType == 0) {
                    outOrderNo = sysJishiOrder.getDepositOutOrderNo();
                } else if (payType == 1) {
                    outOrderNo = sysJishiOrder.getFeeOutOrderNo();
                }
            } else if (orderType == 1) { //套餐计费订单
                SysTaocanOrder sysTaocanOrder = sysTaocanOrderMapper.selectSysTaocanOrderByOrderId(orderId);
                if (Objects.isNull(sysTaocanOrder)) return;
                baseOrder = sysTaocanOrder;
                if (payType == 2) { //支付押金与费用
                    outOrderNo = sysTaocanOrder.getDepositAndFeeOutOrderNo();
                } else if (payType == 3) { //支付超时费用
                    outOrderNo = sysTaocanOrder.getOvertimeFeeOutOrderNo();
                }
            } else if (orderType == 2) { //拼团订单
                SysPintuanChildrenOrder sysPintuanChildrenOrder = sysPintuanChildrenOrderMapper.selectSysPintuanChildrenOrderByOrderId(orderId);
                if (Objects.isNull(sysPintuanChildrenOrder)) return;
                baseOrder = sysPintuanChildrenOrder;
                if (payType == 2) { //支付押金与费用
                    outOrderNo = sysPintuanChildrenOrder.getDepositAndFeeOutOrderNo();
                } else if (payType == 3) { //支付超时费用
                    outOrderNo = sysPintuanChildrenOrder.getOvertimeFeeOutOrderNo();
                }
            }

            if (StringUtils.isEmpty(outOrderNo)) return;

            //向微信支付服务端接口查询订单outOrderNo的支付结果
            Map<String, Object> resultMap = queryPayStatus(outOrderNo);
            //获取支付状态。
            //交易状态，枚举值：
            //SUCCESS：支付成功
            //REFUND：转入退款
            //NOTPAY：未支付
            //CLOSED：已关闭
            //REVOKED：已撤销（仅付款码支付会返回）
            //USERPAYING：用户支付中（仅付款码支付会返回）
            //PAYERROR：支付失败（仅付款码支付会返回）
            //示例值：SUCCESS
            String tradeState = (String) resultMap.get("trade_state");
            if (StringUtils.equals("SUCCESS", tradeState)) { //支付成功
                log.info("查询订单{}的支付状态. result: 已支付成功.", outOrderNo);
                timer.cancel(); //停止当前定时器
                payStatusQueryTimerMap.remove(orderId);

                //获取当前订单用户实际支付的金额(值类型：正整数，单位：分)
                int payerTotal = (int) resultMap.get("payerTotal");
//                SysOrder sysOrder = sysOrderMapper.selectSysOrderByOutOrderNo(outOrderNo);
                //计时计费订单
                if (orderType == 0) {
                    SysJishiOrder sysJishiOrder = (SysJishiOrder) baseOrder;
                    Long scenicId = sysJishiOrder.getScenicId();
                    //先判断当前订单的支付类型。支付类型。0：仅支付押金，1：仅支付订单费用，2：支付押金+费用
                    if (payType == 0) { //仅支付押金
                        //更新订单状态为"待使用"。用户此时已支付了计时计费订单的押金，但还未扫码确认上船。
                        sysJishiOrder.setOrderStatus(OrderStatus.ToUse.getCode());
                        sysJishiOrder.setOrderStatusDesc(OrderStatus.ToUse.getInfo());

                        //更新押金支付状态为“已支付”
                        sysJishiOrder.setDepositPayStatus(PayStatus.Paid.getCode());
                        sysJishiOrder.setDepositPayStatusDesc(PayStatus.Paid.getInfo());
                        double realPayPrice = (new BigDecimal(payerTotal)).divide(new BigDecimal(100), 2, BigDecimal.ROUND_UP).doubleValue();
                        //设置押金实际支付金额 (注意：此处需要将单位从分转为元，且数据格式为double类型，并保留2位小数)
                        sysJishiOrder.setRealPayDeposit(realPayPrice);
                        //设置当前订单用户已实际支付的总金额(此时总金额中只有当前支付的押金金额) (注意：此处需要将单位从分转为元，且数据格式为double类型，并保留2位小数)
                        sysJishiOrder.setRealPayTotalPrice(realPayPrice);

                        //更新数据库中当前计时计费订单的信息
                        sysJishiOrderMapper.updateSysJishiOrder(sysJishiOrder);

                        //更新船只与订单信息关联表中订单的状态
                        UserBoatOrderInfo userBoatOrderInfo = new UserBoatOrderInfo();
                        userBoatOrderInfo.setUserId(sysJishiOrder.getUserId());
                        userBoatOrderInfo.setOrderStatus(OrderStatus.ToUse.getCode());
                        userBoatOrderInfo.setOrderStatusDesc(OrderStatus.ToUse.getInfo());
                        userBoatOrderInfoService.updateBoatOrderInfo(userBoatOrderInfo);

                        //更新船只状态为“上船倒计时中”
                        Long boatId = sysJishiOrder.getBoatId();
                        sysBoatService.updateBoatStatusByBoadId(boatId, BoatStatus.OnBoatCounting);

                        //主动将redis缓存中与当前订单支付相关的key删除
                        CacheUtils.remove(CacheConstants.Order_Pay_Status_Redis_Key, orderId + ":" + orderType);

                        //利用redis实现开启扫码确认上船倒计时
                        // 新创建的计时计费订单在押金支付成功后就会更新为待使用状态，且同时开启上船倒计时，如果超过指定时限仍未扫码确认上船则系统会关闭当前订单，并主动给用户退款。
                        CacheUtils.put(CacheConstants.Order_Confirm_On_Boat_Status_Redis_Key, orderId + ":" + orderType, "", onBoatCountdownDuration, TimeUnit.SECONDS);

                        //更新数据库中景区当日收入表
                        ScenicDailyIncome scenicDailyIncome = scenicDailyIncomeService.selectScenicDailyIncome(new ScenicDailyIncome(scenicId,
                                new Date()));
                        if (Objects.nonNull(scenicDailyIncome)) {
                            BigDecimal totalIncome = scenicDailyIncome.getTotalIncome();
                            BigDecimal orderDepositIncome = scenicDailyIncome.getOrderDepositIncome();
                            BigDecimal newOrderDepositIncome = orderDepositIncome.add(new BigDecimal(realPayPrice));
                            BigDecimal newTotalIncome = totalIncome.add(new BigDecimal(realPayPrice));
                            scenicDailyIncome.setTotalIncome(newTotalIncome);
                            scenicDailyIncome.setOrderDepositIncome(newOrderDepositIncome);
                            scenicDailyIncomeService.updateScenicDailyIncome(scenicDailyIncome);
                        } else {
                            scenicDailyIncome = new ScenicDailyIncome();
                            scenicDailyIncome.setScenicId(scenicId);
                            scenicDailyIncome.setOrderDepositIncome(new BigDecimal(realPayPrice));
                            scenicDailyIncome.setTotalIncome(new BigDecimal(realPayPrice));
                            scenicDailyIncomeService.insertScenicDailyIncome(scenicDailyIncome);
                        }
                    } else if (payType == 1) { //仅支付订单费用
                        //更新订单状态为"已完成"。用户此时已支付了计时计费订单的费用(押金在之前已支付)，说明此时游玩已结束，船已停靠到码头，且船锁已成功上锁。
                        sysJishiOrder.setOrderStatus(OrderStatus.Fulfilled.getCode());
                        sysJishiOrder.setOrderStatusDesc(OrderStatus.Fulfilled.getInfo());

                        //更新订单费用支付状态为“已支付”
                        sysJishiOrder.setFeePayStatus(PayStatus.Paid.getCode());
                        sysJishiOrder.setFeePayStatusDesc(PayStatus.Paid.getInfo());
                        //设置订单费用实际支付金额 (注意：此处需要将单位从分转为元，且数据格式为double类型，并保留2位小数)
                        double realPayFee = (new BigDecimal(payerTotal)).divide(new BigDecimal(100), 2, BigDecimal.ROUND_UP).doubleValue();
                        sysJishiOrder.setRealPayFee(realPayFee);
                        //设置当前订单用户已实际支付的总金额(当前的订单费用+之前已支付的押金金额)
                        sysJishiOrder.setRealPayTotalPrice(realPayFee + sysJishiOrder.getRealPayDeposit());

                        //设置订单关闭时间。此时的计时计费订单才算真正完成，可以关闭了。
                        sysJishiOrder.setClosedTime(new Date());
                        sysJishiOrder.setRemark("订单费用支付成功，订单已完成");

                        //更新数据库中当前计时计费订单的信息
                        sysJishiOrderMapper.updateSysJishiOrder(sysJishiOrder);

                        //删除船只订单关联对象
                        userBoatOrderInfoService.deleteUserBoatOrderInfoByUserId(sysJishiOrder.getUserId());

                        //订单已完成，退还押金
                        new Timer().schedule(new TimerTask() {
                            @Override
                            public void run() {
                                refund(new WxRefundReqBody(orderId, 0, 0, "订单已完成，退还押金"));
                            }
                        }, 5000);

                        //更新数据库中景区当日收入表
                        ScenicDailyIncome scenicDailyIncome = scenicDailyIncomeService.selectScenicDailyIncome(new ScenicDailyIncome(scenicId,
                                new Date()));
                        if (Objects.nonNull(scenicDailyIncome)) {
                            BigDecimal totalIncome = scenicDailyIncome.getTotalIncome();
                            BigDecimal orderFeeIncome = scenicDailyIncome.getOrderFeeIncome();

                            BigDecimal newOrderFeeIncome = orderFeeIncome.add(new BigDecimal(realPayFee));
                            BigDecimal newTotalIncome = totalIncome.add(new BigDecimal(realPayFee));
                            scenicDailyIncome.setTotalIncome(newTotalIncome);
                            scenicDailyIncome.setOrderFeeIncome(newOrderFeeIncome);
                            scenicDailyIncomeService.updateScenicDailyIncome(scenicDailyIncome);
                        }

                        //创建运营动态信息
                        ScenicOperationDynamicInfo scenicOperationDynamicInfo = new ScenicOperationDynamicInfo();
                        scenicOperationDynamicInfo.setOperationType(OperationType.Common_Type);
                        scenicOperationDynamicInfo.setScenicId(scenicId);
                        scenicOperationDynamicInfo.setBoatId(sysJishiOrder.getBoatId());
                        scenicOperationDynamicInfo.setBoatCode(sysJishiOrder.getBoatCode());
                        scenicOperationDynamicInfo.setOrderId(orderId);
                        scenicOperationDynamicInfo.setOrderType(orderType);
                        scenicOperationDynamicInfo.setUserId(sysJishiOrder.getUserId());
                        Date createTime = DateUtils.getNowDate();
                        scenicOperationDynamicInfo.setCreateTime(createTime);
                        IScenicOperationDynamicInfoService operationInfoService = SpringUtils.getBean(IScenicOperationDynamicInfoService.class);
                        scenicOperationDynamicInfo.setContent(String.format("%s, %s船计时计费订单收入%.2f元。", sdf.format(createTime), sysJishiOrder.getBoatCode(), realPayFee));
                        operationInfoService.insertOperationInfo(scenicOperationDynamicInfo);

                        //向景区监控中心发送运营动态信息
                        WebSocketMessageHandler.sendMsgToScenicAdmin(scenicId, scenicOperationDynamicInfo);
                        //给景区监控中心发送景区历史累计总收入被更新的信息
                        //给景区监控中心发送景区历史累计总收入被更新的信息
                        WebSocketMessageHandler.sendScenicAccumulatedTotalIncomeUpdatedMsg(scenicId);

                        return;
                    } else if (payType == 2) {
                        //计时计费订单不存在同时支付押金和订单费用的情况
                    }
                } else if (orderType == 1) {
                    //套餐付费订单
                    SysTaocanOrder sysTaocanOrder = (SysTaocanOrder) baseOrder;
                    Long scenicId = sysTaocanOrder.getScenicId();
                    double orderDeposit = sysTaocanOrder.getOrderDeposit();
                    double orderFee = sysTaocanOrder.getOrderFee();
                    //先判断当前订单的支付类型。支付类型。0：仅支付押金，1：仅支付订单费用，2：支付押金+费用
                    if (payType == 2) { //同时支付押金+费用的情况
                        //更新订单状态为"待使用"。用户此时已支付了押金和费用，但还未确认上船，因此订单状态为待使用。
                        sysTaocanOrder.setOrderStatus(OrderStatus.ToUse.getCode());
                        sysTaocanOrder.setOrderStatusDesc(OrderStatus.ToUse.getInfo());

                        //更新押金支付状态为“已支付”
                        sysTaocanOrder.setDepositPayStatus(PayStatus.Paid.getCode());
                        sysTaocanOrder.setDepositPayStatusDesc(PayStatus.Paid.getInfo());
                        //设置押金实际支付金额
                        sysTaocanOrder.setRealPayDeposit(sysTaocanOrder.getOrderDeposit());
                        //更新订单费用支付状态为“已支付”
                        sysTaocanOrder.setFeePayStatus(PayStatus.Paid.getCode());
                        sysTaocanOrder.setFeePayStatusDesc(PayStatus.Paid.getInfo());
                        //设置订单费用实际支付金额
                        sysTaocanOrder.setRealPayFee(sysTaocanOrder.getOrderFee());
                        //设置当前订单用户已实际支付的总金额(当前的订单费用+之前已支付的押金金额) (注意：此处需要将单位从分转为元，且数据格式为double类型，并保留2位小数)
                        double realPayTotalPrice = (new BigDecimal(payerTotal)).divide(new BigDecimal(100), 2, BigDecimal.ROUND_UP).doubleValue();
                        sysTaocanOrder.setRealPayTotalPrice(realPayTotalPrice);

                        //更新数据库中当前订单的信息
                        sysTaocanOrderMapper.updateSysTaocanOrder(sysTaocanOrder);

                        //更新船只与订单信息关联表中订单的状态
                        UserBoatOrderInfo userBoatOrderInfo = new UserBoatOrderInfo();
                        userBoatOrderInfo.setUserId(sysTaocanOrder.getUserId());
                        userBoatOrderInfo.setOrderStatus(OrderStatus.ToUse.getCode());
                        userBoatOrderInfo.setOrderStatusDesc(OrderStatus.ToUse.getInfo());
                        userBoatOrderInfoService.updateBoatOrderInfo(userBoatOrderInfo);

                        //更新船只状态为“上船倒计时中”
                        Long boatId = sysTaocanOrder.getBoatId();
                        sysBoatService.updateBoatStatusByBoadId(boatId, BoatStatus.OnBoatCounting);

                        //主动将redis缓存中与当前订单支付相关的key删除
                        CacheUtils.remove(CacheConstants.Order_Pay_Status_Redis_Key, orderId + ":" + orderType);
                        //利用redis实现开启扫码确认上船倒计时
                        // 新创建的计时计费订单在押金支付成功后就会更新为待使用状态，且同时开启上船倒计时，如果超过指定时限仍未扫码确认上船则系统会关闭当前订单，并主动给用户退款。
                        CacheUtils.put(CacheConstants.Order_Confirm_On_Boat_Status_Redis_Key, orderId + ":" + orderType, "", onBoatCountdownDuration, TimeUnit.SECONDS);

                        //更新数据库中景区当日收入表
                        ScenicDailyIncome scenicDailyIncome = scenicDailyIncomeService.selectScenicDailyIncome(new ScenicDailyIncome(scenicId,
                                new Date()));
                        if (Objects.nonNull(scenicDailyIncome)) {
                            BigDecimal totalIncome = scenicDailyIncome.getTotalIncome();
                            BigDecimal orderDepositIncome = scenicDailyIncome.getOrderDepositIncome();
                            BigDecimal orderFeeIncome = scenicDailyIncome.getOrderFeeIncome();

                            BigDecimal newOrderDepositIncome = orderDepositIncome.add(new BigDecimal(orderDeposit));
                            BigDecimal newOrderFeeIncome = orderFeeIncome.add(new BigDecimal(orderFee));
                            BigDecimal newTotalIncome = totalIncome.add(new BigDecimal(orderDeposit)).add(new BigDecimal(orderFee));
                            scenicDailyIncome.setTotalIncome(newTotalIncome);
                            scenicDailyIncome.setOrderDepositIncome(newOrderDepositIncome);
                            scenicDailyIncome.setOrderFeeIncome(newOrderFeeIncome);
                            scenicDailyIncomeService.updateScenicDailyIncome(scenicDailyIncome);
                        } else {
                            scenicDailyIncome = new ScenicDailyIncome();
                            scenicDailyIncome.setScenicId(scenicId);
                            scenicDailyIncome.setOrderDepositIncome(new BigDecimal(orderDeposit));
                            scenicDailyIncome.setOrderFeeIncome(new BigDecimal(orderFee));
                            BigDecimal totalIncome = (new BigDecimal(orderDeposit)).add(new BigDecimal(orderFee));
                            scenicDailyIncome.setTotalIncome(totalIncome);
                            scenicDailyIncomeService.insertScenicDailyIncome(scenicDailyIncome);
                        }

                        //创建运营动态信息
                        ScenicOperationDynamicInfo scenicOperationDynamicInfo = new ScenicOperationDynamicInfo();
                        scenicOperationDynamicInfo.setOperationType(OperationType.Common_Type);
                        scenicOperationDynamicInfo.setScenicId(scenicId);
                        scenicOperationDynamicInfo.setBoatId(sysTaocanOrder.getBoatId());
                        scenicOperationDynamicInfo.setBoatCode(sysTaocanOrder.getBoatCode());
                        scenicOperationDynamicInfo.setOrderId(orderId);
                        scenicOperationDynamicInfo.setOrderType(orderType);
                        scenicOperationDynamicInfo.setUserId(sysTaocanOrder.getUserId());
                        Date createTime = DateUtils.getNowDate();
                        scenicOperationDynamicInfo.setCreateTime(createTime);
                        IScenicOperationDynamicInfoService operationInfoService = SpringUtils.getBean(IScenicOperationDynamicInfoService.class);
                        scenicOperationDynamicInfo.setContent(String.format("%s, %s船套餐计费订单套餐费收入%.2f元。", sdf.format(createTime), sysTaocanOrder.getBoatCode(), orderFee));
                        operationInfoService.insertOperationInfo(scenicOperationDynamicInfo);

                        //向景区监控中心发送运营动态信息
                        WebSocketMessageHandler.sendMsgToScenicAdmin(scenicId, scenicOperationDynamicInfo);
                        //给景区监控中心发送景区历史累计总收入被更新的信息
                        WebSocketMessageHandler.sendScenicAccumulatedTotalIncomeUpdatedMsg(scenicId);

                    } else if (payType == 3) {  //支付超时费用
                        //更新订单状态为"已完成"。用户此时已支付了计时计费订单的费用(押金在之前已支付)，说明此时游玩已结束，船已停靠到码头，且船锁已成功上锁。
                        sysTaocanOrder.setOrderStatus(OrderStatus.Fulfilled.getCode());
                        sysTaocanOrder.setOrderStatusDesc(OrderStatus.Fulfilled.getInfo());

                        //更新订单超时费用支付状态为“已支付”
                        sysTaocanOrder.setOvertimeFeePayStatus(PayStatus.Paid.getCode());
                        sysTaocanOrder.setOvertimeFeePayStatusDesc(PayStatus.Paid.getInfo());
                        //设置订单超时费用实际支付金额 (注意：此处需要将单位从分转为元，且数据格式为double类型，并保留2位小数)
                        double realPayOvertimeFee = (new BigDecimal(payerTotal)).divide(new BigDecimal(100), 2, BigDecimal.ROUND_UP).doubleValue();
                        sysTaocanOrder.setRealPayOvertimeFee(realPayOvertimeFee);
                        //设置当前订单用户已实际支付的总金额(当前已支付的押金+套餐费用+超时费用)
                        sysTaocanOrder.setRealPayTotalPrice(sysTaocanOrder.getRealPayDeposit() + sysTaocanOrder.getRealPayFee() + realPayOvertimeFee);

                        //设置订单关闭时间。此时的计时计费订单才算真正完成，可以关闭了。
                        sysTaocanOrder.setClosedTime(new Date());
                        sysTaocanOrder.setRemark("订单费用支付成功，订单已完成");

                        //将船只状态更新为“空闲”。注意：此处无需更新船只信息为"空闲"，因为在船锁锁上时船只的状态就会被更新为“空闲”
//                        SysBoat sysBoat = new SysBoat();
//                        sysBoat.setBoatId(sysTaocanOrder.getBoatId());
//                        sysBoat.setStatus(BoatStatus.Idle.getCode());
//                        sysBoat.setStatusDesc(BoatStatus.Idle.getInfo());
//                        sysBoatMapper.updateSysBoat(sysBoat);

                        //订单已完成，退还押金
                        new Timer().schedule(new TimerTask() {
                            @Override
                            public void run() {
                                refund(new WxRefundReqBody(orderId, 1, 0, "订单已完成，退还押金"));
                            }
                        }, 5000);

                        //更新数据库中当前订单的信息
                        sysTaocanOrderMapper.updateSysTaocanOrder(sysTaocanOrder);

                        //更新数据库中景区当日收入表
                        ScenicDailyIncome scenicDailyIncome = scenicDailyIncomeService.selectScenicDailyIncome(new ScenicDailyIncome(scenicId,
                                new Date()));
                        if (Objects.nonNull(scenicDailyIncome)) {
                            BigDecimal totalIncome = scenicDailyIncome.getTotalIncome();
                            BigDecimal orderFeeIncome = scenicDailyIncome.getOrderFeeIncome();

                            BigDecimal newOrderFeeIncome = orderFeeIncome.add(new BigDecimal(realPayOvertimeFee));
                            BigDecimal newTotalIncome = totalIncome.add(new BigDecimal(realPayOvertimeFee));
                            scenicDailyIncome.setTotalIncome(newTotalIncome);
                            scenicDailyIncome.setOrderFeeIncome(newOrderFeeIncome);
                            scenicDailyIncomeService.updateScenicDailyIncome(scenicDailyIncome);
                        }

                        //创建运营动态信息
                        ScenicOperationDynamicInfo scenicOperationDynamicInfo = new ScenicOperationDynamicInfo();
                        scenicOperationDynamicInfo.setOperationType(OperationType.Common_Type);
                        scenicOperationDynamicInfo.setScenicId(scenicId);
                        scenicOperationDynamicInfo.setBoatId(sysTaocanOrder.getBoatId());
                        scenicOperationDynamicInfo.setBoatCode(sysTaocanOrder.getBoatCode());
                        scenicOperationDynamicInfo.setOrderId(orderId);
                        scenicOperationDynamicInfo.setOrderType(orderType);
                        scenicOperationDynamicInfo.setUserId(sysTaocanOrder.getUserId());
                        Date createTime = DateUtils.getNowDate();
                        scenicOperationDynamicInfo.setCreateTime(createTime);
                        IScenicOperationDynamicInfoService operationInfoService = SpringUtils.getBean(IScenicOperationDynamicInfoService.class);
                        scenicOperationDynamicInfo.setContent(String.format("%s, %s船套餐计费订单超时费收入%.2f元。", sdf.format(createTime), sysTaocanOrder.getBoatCode(), realPayOvertimeFee));
                        operationInfoService.insertOperationInfo(scenicOperationDynamicInfo);

                        //向景区监控中心发送运营动态信息
                        WebSocketMessageHandler.sendMsgToScenicAdmin(scenicId, scenicOperationDynamicInfo);
                        //给景区监控中心发送景区历史累计总收入被更新的信息
                        WebSocketMessageHandler.sendScenicAccumulatedTotalIncomeUpdatedMsg(scenicId);
                    } else {
                        log.error("payType:{}不符合要求，套餐付费订单只存在同时支付押金+费用的情况，即payType只能为2。", payType);
                    }
                } else if (orderType == 2) { //拼团订单
                    SysPintuanChildrenOrder sysPintuanChildrenOrder = (SysPintuanChildrenOrder) baseOrder;
                    Long boatId = sysPintuanChildrenOrder.getBoatId();
                    SysBoat sysBoat = sysBoatMapper.selectSysBoatByBoatId(boatId);

                    Long userId = sysPintuanChildrenOrder.getUserId();

                    //先判断当前订单的支付类型。支付类型。0：仅支付押金，1：仅支付订单费用，2：支付押金+费用
                    if (payType == 2) { //支付押金+费用
                        //删除支付倒计时
                        CacheUtils.remove(CacheConstants.Order_Pay_Status_Redis_Key, orderId + ":" + orderType);

                        //更新当前子订单的状态为"待使用"
                        sysPintuanChildrenOrder.setOrderStatus(OrderStatus.ToUse.getCode()); //更新订单当前状态为待使用(不管此时拼团是否成功，此处都应更新为待使用)
                        sysPintuanChildrenOrder.setOrderStatusDesc(OrderStatus.ToUse.getInfo());

                        //更新押金支付状态为“已支付”
                        sysPintuanChildrenOrder.setDepositPayStatus(PayStatus.Paid.getCode());
                        sysPintuanChildrenOrder.setDepositPayStatusDesc(PayStatus.Paid.getInfo());
                        //设置押金实际支付金额
                        sysPintuanChildrenOrder.setRealPayDeposit(sysPintuanChildrenOrder.getOrderDeposit());

                        //更新订单费用支付状态为“已支付”
                        sysPintuanChildrenOrder.setFeePayStatus(PayStatus.Paid.getCode());
                        sysPintuanChildrenOrder.setFeePayStatusDesc(PayStatus.Paid.getInfo());
                        //设置订单费用实际支付金额
                        sysPintuanChildrenOrder.setRealPayFee(sysPintuanChildrenOrder.getOrderFee());

                        //设置当前订单用户已实际支付的总金额(当前的订单费用+之前已支付的押金金额) (注意：此处需要将单位从分转为元，且数据格式为double类型，并保留2位小数)
                        double realPayTotalPrice = (new BigDecimal(payerTotal)).divide(new BigDecimal(100), 2, BigDecimal.ROUND_UP).doubleValue();
                        sysPintuanChildrenOrder.setRealPayTotalPrice(realPayTotalPrice);

                        //先判断当前用户是否是当前拼团订单的首个创建者
                        long expireTime = CacheUtils.getExpire(CacheConstants.Pintuan_Order_Redis_Key, String.valueOf(boatId));
                        SysPintuanParentOrder sysPintuanParentOrder;
                        if (expireTime <= 0) {
                            //指定boatId的船只当前未在拼团中，因此当前用户是此拼团订单的发起人

                            //1，创建并初始化拼团父订单对象SysPintuanParentOrder
                            sysPintuanParentOrder = new SysPintuanParentOrder();
                            BeanUtils.copyProperties(sysPintuanChildrenOrder, sysPintuanParentOrder);
                            //设置父订单id
                            String parentOrderId = ShortIdUtils.generateOrderIdStr();
                            sysPintuanParentOrder.setOrderId(parentOrderId);
                            //设置实际参与拼团人数
                            sysPintuanParentOrder.setPintuanPersonNum(sysPintuanChildrenOrder.getPintuanPersonNum());

                            //2，设置当前子订单的父订单id
                            sysPintuanChildrenOrder.setParentOrderId(parentOrderId);

                            //根据用户id查询对应的用户信息。
                            SysUser sysUser = sysUserService.selectUserById(userId);

                            //3，创建并初始化船只订单关联表
                            UserBoatOrderInfo userBoatOrderInfo = new UserBoatOrderInfo();
                            userBoatOrderInfo.setUserId(userId);
                            if (Objects.nonNull(sysUser)) {
                                userBoatOrderInfo.setUserPhone(sysUser.getPhonenumber());
                            }
                            userBoatOrderInfo.setBoatId(boatId);
                            userBoatOrderInfo.setBoatIdentityId(sysBoat.getBoatIdentityId());
                            userBoatOrderInfo.setOrderId(parentOrderId); //此处设置的是父订单id
                            userBoatOrderInfo.setOrderStatus(OrderStatus.ToUse.getCode()); //设置拼团订单当前状态为待使用
                            userBoatOrderInfo.setOrderType(2); //设置订单类型为拼团订单
                            userBoatOrderInfoService.insertBoatOrderInfo(userBoatOrderInfo);

                            //判断当前请求拼团人数是否等于当前拼团类型允许的最大拼团人数(此处不会存在大于的情况，因为在之前创建当前子订单时会判断此种情况，如果大于会抛出异常)
                            if (Objects.equals(sysPintuanChildrenOrder.getPintuanPersonNum(), sysPintuanChildrenOrder.getPintuanAllowPersonNum())) {
                                //此时已拼团成功，无需开启拼团倒计时，直接通知当前用户拼团成功
                                Channel miniProgramChannel = WebSocketChannelManager.getMiniprogramWebsocketChannelByUserId(userId);
                                if (Objects.nonNull(miniProgramChannel)) {
                                    PintuanStatusMsg pintuanStatusMsg = new PintuanStatusMsg();
                                    pintuanStatusMsg.setBoatId(boatId);
                                    pintuanStatusMsg.setStatus(2); //设置拼团状态。0: 拼团中，1：拼团失败。2：拼团成功。
                                    WebSocketMessageHandler.notifyClientSidePintuanStatusModified(miniProgramChannel, pintuanStatusMsg);
                                }

                                log.info("拼团成功！拼团父订单id：{}", parentOrderId);

                                //将子订单的拼团状态设置为“拼团成功”
                                sysPintuanChildrenOrder.setPintuanStatus(PintuanStatus.GroupBookingSucceed.getCode());
                                sysPintuanChildrenOrder.setPintuanStatusDesc(PintuanStatus.GroupBookingSucceed.getInfo());

                                //将父订单的拼团状态设置为“拼团成功”
                                sysPintuanParentOrder.setPintuanStatus(PintuanStatus.GroupBookingSucceed.getCode());
                                sysPintuanParentOrder.setPintuanStatusDesc(PintuanStatus.GroupBookingSucceed.getInfo());

                                //将船只状态从"空闲"更新为“上船倒计时中”
                                sysBoat.setStatus(BoatStatus.OnBoatCounting.getCode());
                                sysBoat.setStatusDesc(BoatStatus.OnBoatCounting.getInfo());

                                //拼团成功后，还需开启上船倒计时
                                CacheUtils.put(CacheConstants.Order_Confirm_On_Boat_Status_Redis_Key, parentOrderId + ":" + orderType, "", onBoatCountdownDuration, TimeUnit.SECONDS);
                            } else {
                                //此时拼团未成功，需开启拼团倒计时
                                CacheUtils.put(CacheConstants.Pintuan_Order_Redis_Key, String.valueOf(boatId), "", pintuanCountdownDuration, TimeUnit.SECONDS);

                                //将船只状态从"空闲"更新为“拼团中”
                                sysBoat.setStatus(BoatStatus.GroupBooking.getCode());
                                sysBoat.setStatusDesc(BoatStatus.GroupBooking.getInfo());

                                //将子订单的拼团状态设置为“拼团中”
                                sysPintuanChildrenOrder.setPintuanStatus(PintuanStatus.GroupBooking.getCode());
                                sysPintuanChildrenOrder.setPintuanStatusDesc(PintuanStatus.GroupBooking.getInfo());

                                //将父订单的拼团状态设置为“拼团中”
                                sysPintuanParentOrder.setPintuanStatus(PintuanStatus.GroupBooking.getCode());
                                sysPintuanParentOrder.setPintuanStatusDesc(PintuanStatus.GroupBooking.getInfo());

                                log.info("开启新的拼团，拼团父订单id：{}", parentOrderId);
                            }

                            //设置父订单的状态为"待使用"。拼团父订单在订单开始计费前的状态都为"待使用".
                            sysPintuanParentOrder.setOrderStatus(OrderStatus.ToUse.getCode());
                            sysPintuanParentOrder.setOrderStatusDesc(OrderStatus.ToUse.getInfo());

                            //将sysPintuanParentOrder插入数据库
                            sysPintuanParentOrderMapper.insertSysPintuanParentOrder(sysPintuanParentOrder);
                        } else {
                            //当前船只正在拼团中，当前用户非此拼团订单的发起人
                            sysPintuanParentOrder = sysPintuanParentOrderMapper.selectSysPintuanParentOrderByOrderId(sysPintuanChildrenOrder.getParentOrderId());
                            List<SysPintuanChildrenOrder> pintuanChildrenOrderList = sysPintuanParentOrder.getPintuanChildrenOrderList();
                            //先判断当前拼团是否成功
                            int pintuanAllowPersonNum = sysPintuanParentOrder.getPintuanAllowPersonNum();
                            //注意：如下判断基本可认为不存在大于的情况，因为在用户请求支付拼团订单的押金与费用前会先检查下当时请求拼团的人数是否已超过允许的最大人数
                            if (sysPintuanChildrenOrder.getPintuanPersonNum() + sysPintuanParentOrder.getPintuanPersonNum() == pintuanAllowPersonNum) {
                                //此时已拼团成功，需关闭拼团倒计时，并通知所有参与拼团的用户拼团成功
                                log.info("拼团成功！拼团父订单id：{}", sysPintuanParentOrder.getOrderId());

                                //将子订单的拼团状态设置为“拼团成功”
                                sysPintuanChildrenOrder.setPintuanStatus(PintuanStatus.GroupBookingSucceed.getCode());
                                sysPintuanChildrenOrder.setPintuanStatusDesc(PintuanStatus.GroupBookingSucceed.getInfo());

                                //将父订单的拼团状态设置为“拼团成功”
                                sysPintuanParentOrder.setPintuanStatus(PintuanStatus.GroupBookingSucceed.getCode());
                                sysPintuanParentOrder.setPintuanStatusDesc(PintuanStatus.GroupBookingSucceed.getInfo());

                                //关闭拼团倒计时
                                CacheUtils.remove(CacheConstants.Pintuan_Order_Redis_Key, String.valueOf(sysPintuanParentOrder.getBoatId()));

                                //通知所有参与拼团的用户拼团成功了
                                pintuanChildrenOrderList.forEach(childrenOrder -> {
                                    Channel miniProgramChannel = WebSocketChannelManager.getMiniprogramWebsocketChannelByUserId(userId);
                                    if (Objects.nonNull(miniProgramChannel)) {
                                        PintuanStatusMsg pintuanStatusMsg = new PintuanStatusMsg();
                                        pintuanStatusMsg.setBoatId(childrenOrder.getBoatId());
                                        pintuanStatusMsg.setStatus(2); //设置拼团状态。0: 拼团中，1：拼团失败。2：拼团成功。
                                        WebSocketMessageHandler.notifyClientSidePintuanStatusModified(miniProgramChannel, pintuanStatusMsg);
                                    }
                                });

                                //拼团成功后，还需开启上船倒计时
                                CacheUtils.put(CacheConstants.Order_Confirm_On_Boat_Status_Redis_Key, sysPintuanParentOrder.getOrderId() + ":" + orderType, "", 60, TimeUnit.SECONDS);

                                //将船只状态从"拼团中"更新为“上船倒计时中”
                                sysBoat.setStatus(BoatStatus.OnBoatCounting.getCode());
                                sysBoat.setStatusDesc(BoatStatus.OnBoatCounting.getInfo());
                            } else {
                                //此时拼团还未成功，还需继续拼团

                                //将订单的拼团状态设置为“拼团中”，父订单的拼团状态此时无需修改。
                                sysPintuanChildrenOrder.setPintuanStatus(PintuanStatus.GroupBooking.getCode());
                                sysPintuanChildrenOrder.setPintuanStatusDesc(PintuanStatus.GroupBooking.getInfo());
                            }

                            //设置当前最新的实际参与拼团人数
                            sysPintuanParentOrder.setPintuanPersonNum(sysPintuanChildrenOrder.getPintuanPersonNum() + sysPintuanParentOrder.getPintuanPersonNum());
                            //更新数据库中的父订单对象
                            sysPintuanParentOrderMapper.updateSysPintuanParentOrder(sysPintuanParentOrder);
                        }

                        //给子订单设置父订单id
                        sysPintuanChildrenOrder.setParentOrderId(sysPintuanParentOrder.getOrderId());
                        //更新数据库中的当前子订单对象
                        sysPintuanChildrenOrderMapper.updateSysPintuanChildrenOrder(sysPintuanChildrenOrder);

                        //更新船只状态
                        sysBoatMapper.updateSysBoat(sysBoat);
                    } else if (payType == 3) { //支付超时费用
                        //更新订单状态为"已完成"。用户此时已支付了计时计费订单的费用(押金在之前已支付)，说明此时游玩已结束，船已停靠到码头，且船锁已成功上锁。
                        sysPintuanChildrenOrder.setOrderStatus(OrderStatus.Fulfilled.getCode());
                        sysPintuanChildrenOrder.setOrderStatusDesc(OrderStatus.Fulfilled.getInfo());

                        //更新订单超时费用支付状态为“已支付”
                        sysPintuanChildrenOrder.setOvertimeFeePayStatus(PayStatus.Paid.getCode());
                        sysPintuanChildrenOrder.setOvertimeFeePayStatusDesc(PayStatus.Paid.getInfo());
                        //设置订单超时费用实际支付金额 (注意：此处需要将单位从分转为元，且数据格式为double类型，并保留2位小数)
                        double realPayOvertimeFee = (new BigDecimal(payerTotal)).divide(new BigDecimal(100), 2, BigDecimal.ROUND_UP).doubleValue();
                        sysPintuanChildrenOrder.setRealPayOvertimeFee(realPayOvertimeFee);
                        //设置当前订单用户已实际支付的总金额(当前已支付的押金+套餐费用+超时费用)
                        sysPintuanChildrenOrder.setRealPayTotalPrice(sysPintuanChildrenOrder.getRealPayDeposit() + sysPintuanChildrenOrder.getRealPayFee() + realPayOvertimeFee);

                        //设置订单关闭时间。此时的计时计费订单才算真正完成，可以关闭了。
                        sysPintuanChildrenOrder.setClosedTime(new Date());
                        sysPintuanChildrenOrder.setRemark("订单费用支付成功，订单已完成");

                        //订单已完成，退还押金
                        new Timer().schedule(new TimerTask() {
                            @Override
                            public void run() {
                                refund(new WxRefundReqBody(orderId, 2, 0, "订单已完成，退还押金"));
                            }
                        }, 5000);

                        //更新数据库中当前订单的信息
                        sysPintuanChildrenOrderMapper.updateSysPintuanChildrenOrder(sysPintuanChildrenOrder);

                        //检查是否所有已扫码上船的用户都已完成并关闭订单
                        SysPintuanParentOrder sysPintuanParentOrder = sysPintuanParentOrderMapper.selectSysPintuanParentOrderByOrderId(sysPintuanChildrenOrder.getParentOrderId());
                        if (Objects.isNull(sysPintuanParentOrder)) return;
                        List<SysPintuanChildrenOrder> pintuanChildrenOrderList = sysPintuanParentOrder.getPintuanChildrenOrderList();

                        boolean notAllOrderFulfilled = false;
                        for (SysPintuanChildrenOrder childrenOrder1 : pintuanChildrenOrderList) {
                            if (childrenOrder1.getOnBoatStatus() == 1 && childrenOrder1.getOrderStatus() != OrderStatus.Fulfilled.getCode()) {
                                //此时说明还有已上船的子订单未完成
                                notAllOrderFulfilled = true;
                                break;
                            }
                        }

                        if (!notAllOrderFulfilled) { //所有已扫码上船的用户都已完成并关闭订单
                            sysPintuanParentOrder.setOrderStatus(OrderStatus.Fulfilled.getCode());
                            sysPintuanParentOrder.setOrderStatusDesc(OrderStatus.Fulfilled.getInfo());
                            sysPintuanParentOrderMapper.updateSysPintuanParentOrder(sysPintuanParentOrder);

                            //删除船只订单关联对象
                            userBoatOrderInfoService.deleteUserBoatOrderInfoByUserId(sysPintuanParentOrder.getUserId());
                        }

                    } else {
                        log.error("payType:{}不符合要求，拼团订单只存在同时支付押金+费用的情况，即payType只能为2。", payType);
                    }
                }
            } else {
                log.info("查询订单{}的支付状态. result: 未支付.", outOrderNo);
            }
        }
    }

    /**
     * 检查退款状态的定时任务。
     */
    class CheckRefundStatusTask extends TimerTask {

        /**
         * 订单id。
         */
        private String orderId;
        /**
         * 订单类型。
         */
        private int orderType;
        /**
         * 退款类型。0：只退押金，1：只退订单费用，2：退押金+订单费用。
         */
        private int refundType;
        /**
         * 退款单号。
         */
        private String outRefundNo;
        private Timer timer;

        private int count;
        private int maxTryNum = 28800;

        public CheckRefundStatusTask(String orderId, int orderType, int refundType, String outRefundNo) {
            this.orderId = orderId;
            this.orderType = orderType;
            this.refundType = refundType;
            this.outRefundNo = outRefundNo;
        }

        @Override
        public void run() {
            if (timer == null) {
                timer = payStatusQueryTimerMap.get(orderId);
            }

            count++;
            if (count >= maxTryNum) { //超过最大查询次数后取消定时器，不再继续查询
                timer.cancel();
                payStatusQueryTimerMap.remove(orderId);
            }

            //根据退款单号查询退款状态
            Map<String, Object> resultMap = queryRefundStatus(outRefundNo);
            //商户退款单号
//            resultMap.put("out_refund_no", resultObj.getString("out_refund_no"));
            //商户订单号
//            resultMap.put("out_trade_no", resultObj.getString("out_trade_no"));

            //获取退款状态
            //状态枚举值：
            //SUCCESS：退款成功
            //CLOSED：退款关闭
            //PROCESSING：退款处理中
            //ABNORMAL：退款异常
            //示例值：SUCCESS
            String status = (String) resultMap.get("status");
            Integer refund = (Integer) resultMap.get("refund");

            if (StringUtils.equals("SUCCESS", status) && Objects.nonNull(refund)) { //退款成功
                //将退款金额单位从分转为“元”，且保留2位小数.
                BigDecimal refundAmount = (new BigDecimal(refund)).divide(new BigDecimal(100), 2, BigDecimal.ROUND_UP);
                log.info("查询退款单号{}的退款状态. result: 已退款成功, 退款金额: {}", outRefundNo, refundAmount.doubleValue());
                timer.cancel(); //退款成功，主动停止查询定时器
                payStatusQueryTimerMap.remove(orderId);

                if (orderType == 0) { //计时计费订单
                    SysJishiOrder sysJishiOrder = sysJishiOrderMapper.selectSysJishiOrderByOrderId(orderId);
                    Long scenicId = sysJishiOrder.getScenicId();
                    if (refundType == 0) { //只退押金
                        sysJishiOrder.setDepositPayStatus(PayStatus.Refund.getCode()); //将押金支付状态修改为：已退款
                        sysJishiOrder.setDepositPayStatusDesc(PayStatus.Refund.getInfo());

                        ScenicDailyIncome scenicDailyIncome = scenicDailyIncomeService.selectScenicDailyIncome(new ScenicDailyIncome(scenicId,
                                DateUtils.getNowDate()));
                        if (Objects.nonNull(scenicDailyIncome)) {
                            BigDecimal totalIncome = scenicDailyIncome.getTotalIncome();
                            BigDecimal orderDepositIncome = scenicDailyIncome.getOrderDepositIncome();
                            BigDecimal newOrderDepositIncome = orderDepositIncome.subtract(refundAmount);
                            BigDecimal newTotalIncome = totalIncome.subtract(refundAmount);
                            if (newOrderDepositIncome.doubleValue() < 0) {
                                scenicDailyIncome.setOrderDepositIncome(new BigDecimal(0));
                            } else {
                                scenicDailyIncome.setOrderDepositIncome(newOrderDepositIncome);
                            }
                            if (newTotalIncome.doubleValue() < 0) {
                                scenicDailyIncome.setTotalIncome(new BigDecimal(0));
                            } else {
                                scenicDailyIncome.setTotalIncome(newTotalIncome);
                            }
                            scenicDailyIncomeService.updateScenicDailyIncome(scenicDailyIncome);
                        }
                    } else if (refundType == 1) { //只退订单费用
                        sysJishiOrder.setFeePayStatus(PayStatus.Refund.getCode()); //将订单费用支付状态修改为：已退款
                        sysJishiOrder.setFeePayStatusDesc(PayStatus.Refund.getInfo());
                    } else if (refundType == 2) { //退订单全部已支付金额，即押金+费用。
                        //计时计费订单无法同时退押金和订单费用
                    }

                    //更新数据库中订单信息
                    sysJishiOrderMapper.updateSysJishiOrder(sysJishiOrder);
                } else if (orderType == 1) { //套餐计费订单
                    SysTaocanOrder sysTaocanOrder = sysTaocanOrderMapper.selectSysTaocanOrderByOrderId(orderId);
                    Long scenicId = sysTaocanOrder.getScenicId();
                    if (refundType == 0) { //只退押金
                        sysTaocanOrder.setDepositPayStatus(PayStatus.Refund.getCode()); //将押金支付状态修改为：已退款
                        sysTaocanOrder.setDepositPayStatusDesc(PayStatus.Refund.getInfo());
                    } else if (refundType == 2) { //退订单全部已支付金额，即押金+费用。
                        sysTaocanOrder.setDepositPayStatus(PayStatus.Refund.getCode()); //将押金支付状态修改为：已退款
                        sysTaocanOrder.setDepositPayStatusDesc(PayStatus.Refund.getInfo());
                        sysTaocanOrder.setFeePayStatus(PayStatus.Refund.getCode()); //将订单费用支付状态修改为：已退款
                        sysTaocanOrder.setFeePayStatusDesc(PayStatus.Refund.getInfo());

                        ScenicDailyIncome scenicDailyIncome = scenicDailyIncomeService.selectScenicDailyIncome(new ScenicDailyIncome(scenicId,
                                DateUtils.getNowDate()));
                        if (Objects.nonNull(scenicDailyIncome)) {
                            BigDecimal totalIncome = scenicDailyIncome.getTotalIncome();
                            BigDecimal orderDepositIncome = scenicDailyIncome.getOrderDepositIncome();
                            BigDecimal orderFeeIncome = scenicDailyIncome.getOrderFeeIncome();

                            BigDecimal newOrderDepositIncome = orderDepositIncome.subtract(new BigDecimal(sysTaocanOrder.getRealPayDeposit()));
                            BigDecimal newOrderFeeIncome = orderFeeIncome.subtract(new BigDecimal(sysTaocanOrder.getRealPayFee()));
                            BigDecimal newTotalIncome = totalIncome.subtract(new BigDecimal(sysTaocanOrder.getRealPayDeposit()))
                                    .subtract(new BigDecimal(sysTaocanOrder.getRealPayFee()));
                            if (newOrderDepositIncome.doubleValue() < 0) {
                                scenicDailyIncome.setOrderDepositIncome(new BigDecimal(0));
                            } else {
                                scenicDailyIncome.setOrderDepositIncome(newOrderDepositIncome);
                            }
                            if (newOrderFeeIncome.doubleValue() < 0) {
                                scenicDailyIncome.setOrderFeeIncome(new BigDecimal(0));
                            } else {
                                scenicDailyIncome.setOrderFeeIncome(newOrderFeeIncome);
                            }
                            if (newTotalIncome.doubleValue() < 0) {
                                scenicDailyIncome.setTotalIncome(new BigDecimal(0));
                            } else {
                                scenicDailyIncome.setTotalIncome(newTotalIncome);
                            }
                            scenicDailyIncomeService.updateScenicDailyIncome(scenicDailyIncome);
                        }

                        //创建运营动态信息
                        ScenicOperationDynamicInfo scenicOperationDynamicInfo = new ScenicOperationDynamicInfo();
                        scenicOperationDynamicInfo.setOperationType(OperationType.Common_Type);
                        scenicOperationDynamicInfo.setScenicId(scenicId);
                        scenicOperationDynamicInfo.setBoatId(sysTaocanOrder.getBoatId());
                        scenicOperationDynamicInfo.setBoatCode(sysTaocanOrder.getBoatCode());
                        scenicOperationDynamicInfo.setOrderId(orderId);
                        scenicOperationDynamicInfo.setOrderType(orderType);
                        scenicOperationDynamicInfo.setUserId(sysTaocanOrder.getUserId());
                        Date createTime = DateUtils.getNowDate();
                        scenicOperationDynamicInfo.setCreateTime(createTime);
                        IScenicOperationDynamicInfoService operationInfoService = SpringUtils.getBean(IScenicOperationDynamicInfoService.class);
                        scenicOperationDynamicInfo.setContent(String.format("%s, %s船因订单被取消或关闭，退还用户已预支付的套餐费%.2f元。", sdf.format(createTime), sysTaocanOrder.getBoatCode(), sysTaocanOrder.getRealPayFee()));
                        operationInfoService.insertOperationInfo(scenicOperationDynamicInfo);

                        //向景区监控中心发送运营动态信息
                        WebSocketMessageHandler.sendMsgToScenicAdmin(scenicId, scenicOperationDynamicInfo);
                        //给景区监控中心发送景区历史累计总收入被更新的信息
                        WebSocketMessageHandler.sendScenicAccumulatedTotalIncomeUpdatedMsg(scenicId);
                    } else if (refundType == 3) { //退超时费用
                        sysTaocanOrder.setOvertimeFeePayStatus(PayStatus.Refund.getCode()); //将超时费用的支付状态修改为：已退款
                        sysTaocanOrder.setOvertimeFeePayStatusDesc(PayStatus.Refund.getInfo());
                    } else {
                    }

                    sysTaocanOrderMapper.updateSysTaocanOrder(sysTaocanOrder);
                } else if (orderType == 2) { //拼团订单
                    SysPintuanChildrenOrder sysPintuanChildrenOrder = sysPintuanChildrenOrderMapper.selectSysPintuanChildrenOrderByOrderId(orderId);
                    if (refundType == 0) { //只退押金
                        sysPintuanChildrenOrder.setDepositPayStatus(PayStatus.Refund.getCode()); //将押金支付状态修改为：已退款
                        sysPintuanChildrenOrder.setDepositPayStatusDesc(PayStatus.Refund.getInfo());
                    } else if (refundType == 2) { //退订单全部已支付金额，即押金+费用。
                        sysPintuanChildrenOrder.setDepositPayStatus(PayStatus.Refund.getCode()); //将押金支付状态修改为：已退款
                        sysPintuanChildrenOrder.setDepositPayStatusDesc(PayStatus.Refund.getInfo());
                        sysPintuanChildrenOrder.setFeePayStatus(PayStatus.Refund.getCode()); //将订单费用支付状态修改为：已退款
                        sysPintuanChildrenOrder.setFeePayStatusDesc(PayStatus.Refund.getInfo());
                    } else if (refundType == 3) { //退超时费用
                        sysPintuanChildrenOrder.setOvertimeFeePayStatus(PayStatus.Refund.getCode()); //将超时费用的支付状态修改为：已退款
                        sysPintuanChildrenOrder.setOvertimeFeePayStatusDesc(PayStatus.Refund.getInfo());
                    } else {
                    }

                    sysPintuanChildrenOrderMapper.updateSysPintuanChildrenOrder(sysPintuanChildrenOrder);
                }

            } else if (StringUtils.equals("PROCESSING", status)) { //退款中
                log.info("查询退款单号{}的退款状态. result: 退款中...", outRefundNo);
            } else {
                log.info("查询退款单号{}的退款状态. result: 退款中...", outRefundNo);
            }
        }
    }
}
